fix(telegram): fail closed on unverified delivery
This commit is contained in:
@@ -96,7 +96,7 @@ def test_agent99_v5_formatter_synthetic_gate_loads_model_dependencies() -> None:
|
||||
assert 'C:\\Wooo' in synthetic
|
||||
|
||||
|
||||
def test_agent99_telegram_threads_source_alert_and_persists_lifecycle() -> None:
|
||||
def test_agent99_telegram_lifecycle_fails_closed_without_direct_sender() -> None:
|
||||
source = CONTROL.read_text(encoding="utf-8")
|
||||
inbox = SRE_INBOX.read_text(encoding="utf-8")
|
||||
|
||||
@@ -104,8 +104,6 @@ def test_agent99_telegram_threads_source_alert_and_persists_lifecycle() -> None:
|
||||
assert "replyMessageId = $replyMessageId" in inbox
|
||||
assert 'PSObject.Properties["replyMessageId"]' in source
|
||||
assert 'PSObject.Properties["correlationKey"]' in source
|
||||
assert '"reply_parameters"' in source
|
||||
assert 'telegram_receipt_b64=' in source
|
||||
assert 'schemaVersion = "agent99_telegram_incident_state_v1"' in source
|
||||
assert 'rootMessageId = $rootMessageId' in source
|
||||
assert 'reason = "same_lifecycle_state"' in source
|
||||
@@ -113,6 +111,12 @@ def test_agent99_telegram_threads_source_alert_and_persists_lifecycle() -> None:
|
||||
assert "lastRunKey = $Card.runKey" in source
|
||||
assert "$previousRunKey -eq [string]$incidentCard.runKey" in source
|
||||
assert '$dedupeParts += "run=$($incidentCard.runKey)"' in source
|
||||
assert 'error = "canonical_telegram_gateway_transport_required"' in source
|
||||
assert 'providerSendPerformed = $false' in source
|
||||
assert 'routeStatus = "blocked_no_egress"' in source
|
||||
assert "function Send-AgentTelegramRelay" not in source
|
||||
assert "function Send-AgentTelegramPhotoDirect" not in source
|
||||
assert "telegram_receipt_b64=" not in source
|
||||
|
||||
|
||||
def test_agent99_records_deduped_telegram_as_an_explicit_attempt() -> None:
|
||||
@@ -495,7 +499,7 @@ def test_agent99_bounds_stale_ssh_processes_without_touching_tunnels() -> None:
|
||||
source = CONTROL.read_text(encoding="utf-8")
|
||||
config = json.loads((ROOT / "agent99.config.99.example.json").read_text())
|
||||
guard = source[source.index("function Invoke-AgentStaleSshProcessGuard") :]
|
||||
guard = guard[: guard.index("function Send-AgentTelegramRelay")]
|
||||
guard = guard[: guard.index("function Get-AgentObjectValue")]
|
||||
|
||||
assert 'Get-CimInstance Win32_Process -Filter "Name=\'ssh.exe\'"' in guard
|
||||
assert 'CommandLine -cmatch "(^|\\s)-[NLRD](\\s|$)"' in guard
|
||||
|
||||
@@ -14,6 +14,37 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import src.services.telegram_gateway as gateway_module
|
||||
|
||||
_SENT_CHAT_ID = -1001
|
||||
_SENT_DESTINATION_BINDING = gateway_module._telegram_destination_binding(
|
||||
_SENT_CHAT_ID
|
||||
)
|
||||
_SENT_RECEIPT = {
|
||||
"ok": True,
|
||||
"_awooop_delivery_status": "sent",
|
||||
"_awooop_provider_send_performed": True,
|
||||
"result": {"message_id": 42, "chat": {"id": _SENT_CHAT_ID}},
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
"schema_version": "telegram_canonical_egress_receipt_v1",
|
||||
"decision": "allowed",
|
||||
"destination_alias": "awoooi_sre_war_room",
|
||||
"destination_binding": _SENT_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": _SENT_DESTINATION_BINDING,
|
||||
"payload_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"provider_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"destination_binding_verified": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class FakeRedis:
|
||||
"""模擬 Redis 行為,記錄 set/get/delete 呼叫"""
|
||||
@@ -78,7 +109,7 @@ def gateway_with_fake_redis():
|
||||
gw = TelegramGateway.__new__(TelegramGateway) # 跳過 __init__
|
||||
gw._initialized = True
|
||||
gw._last_message_time = None
|
||||
gw.send_to_group = AsyncMock()
|
||||
gw.send_to_group = AsyncMock(return_value=dict(_SENT_RECEIPT))
|
||||
gw.send_notification = AsyncMock()
|
||||
|
||||
fake_redis = FakeRedis()
|
||||
|
||||
@@ -20,6 +20,31 @@ from src.services.telegram_gateway import TelegramGateway
|
||||
|
||||
_CHAT_ID = "verified-chat"
|
||||
_MESSAGE_ID = 77
|
||||
_DESTINATION_BINDING = gateway_module._telegram_destination_binding(_CHAT_ID)
|
||||
_SENT_RECEIPT = {
|
||||
"ok": True,
|
||||
"_awooop_delivery_status": "sent",
|
||||
"_awooop_provider_send_performed": True,
|
||||
"result": {"message_id": _MESSAGE_ID, "chat": {"id": _CHAT_ID}},
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
"schema_version": "telegram_canonical_egress_receipt_v1",
|
||||
"decision": "allowed",
|
||||
"destination_alias": "inbound_interaction_reply_target",
|
||||
"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": "inbound_interaction_reply_target",
|
||||
"destination_binding": _DESTINATION_BINDING,
|
||||
"payload_destination_binding": _DESTINATION_BINDING,
|
||||
"provider_destination_binding": _DESTINATION_BINDING,
|
||||
"destination_binding_verified": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _assert_verified_callback_payload(payload: dict) -> None:
|
||||
@@ -174,13 +199,15 @@ async def test_llm_and_category_results_keep_verified_callback_context(
|
||||
|
||||
class _Redis:
|
||||
async def get(self, _key: str) -> str:
|
||||
return json.dumps({
|
||||
"name": "inspect",
|
||||
"provider": "internal",
|
||||
"tool": "status",
|
||||
"risk": "low",
|
||||
"incident_id": "INC-1",
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
"name": "inspect",
|
||||
"provider": "internal",
|
||||
"tool": "status",
|
||||
"risk": "low",
|
||||
"incident_id": "INC-1",
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(gateway_module, "get_redis", lambda: _Redis())
|
||||
monkeypatch.setattr(
|
||||
@@ -213,13 +240,15 @@ async def test_llm_and_category_results_keep_verified_callback_context(
|
||||
monkeypatch.setattr(
|
||||
callback_dispatcher,
|
||||
"dispatch_action",
|
||||
AsyncMock(return_value=DispatchResult(
|
||||
success=True,
|
||||
action="inspect",
|
||||
incident_id="INC-1",
|
||||
user_id=42,
|
||||
result_text="result",
|
||||
)),
|
||||
AsyncMock(
|
||||
return_value=DispatchResult(
|
||||
success=True,
|
||||
action="inspect",
|
||||
incident_id="INC-1",
|
||||
user_id=42,
|
||||
result_text="result",
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
class _IncidentRepo:
|
||||
@@ -264,7 +293,9 @@ async def test_write_category_callback_keeps_verified_context(
|
||||
"verify_callback",
|
||||
AsyncMock(return_value={"id": 42}),
|
||||
)
|
||||
monkeypatch.setattr(gateway, "_check_incident_state_guard", AsyncMock(return_value=None))
|
||||
monkeypatch.setattr(
|
||||
gateway, "_check_incident_state_guard", AsyncMock(return_value=None)
|
||||
)
|
||||
monkeypatch.setattr(gateway, "_answer_callback", AsyncMock())
|
||||
spec = SimpleNamespace(
|
||||
requires_multi_sig=False,
|
||||
@@ -278,13 +309,15 @@ async def test_write_category_callback_keeps_verified_context(
|
||||
monkeypatch.setattr(
|
||||
callback_dispatcher,
|
||||
"dispatch_action",
|
||||
AsyncMock(return_value=DispatchResult(
|
||||
success=True,
|
||||
action="restart_safe",
|
||||
incident_id="INC-1",
|
||||
user_id=42,
|
||||
result_text="done",
|
||||
)),
|
||||
AsyncMock(
|
||||
return_value=DispatchResult(
|
||||
success=True,
|
||||
action="restart_safe",
|
||||
incident_id="INC-1",
|
||||
user_id=42,
|
||||
result_text="done",
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
class _IncidentRepo:
|
||||
@@ -335,9 +368,7 @@ async def test_approval_replies_keep_verified_callback_context(
|
||||
_assert_verified_callback_payload(status_payload)
|
||||
source_extra = status_payload[gateway_module._AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY]
|
||||
merged = gateway_module._merge_outbound_source_envelope_extra({}, source_extra)
|
||||
assert merged["callback_reply"]["parent_provider_message_id"] == str(
|
||||
_MESSAGE_ID
|
||||
)
|
||||
assert merged["callback_reply"]["parent_provider_message_id"] == str(_MESSAGE_ID)
|
||||
|
||||
send_request.reset_mock()
|
||||
approval_id = UUID("44444444-4444-4444-4444-444444444444")
|
||||
@@ -367,8 +398,9 @@ async def test_approval_replies_keep_verified_callback_context(
|
||||
@pytest.mark.parametrize(
|
||||
("result", "expected"),
|
||||
[
|
||||
({"ok": True}, True),
|
||||
({"ok": True, "_awooop_delivery_status": "sent_reused"}, True),
|
||||
(_SENT_RECEIPT, True),
|
||||
({"ok": True}, False),
|
||||
({"ok": True, "_awooop_delivery_status": "sent_reused"}, False),
|
||||
({"ok": False}, False),
|
||||
(
|
||||
{"ok": True, "_awooop_delivery_status": "blocked_no_egress"},
|
||||
@@ -414,7 +446,7 @@ async def test_approval_structured_no_send_retries_plain_on_exact_parent(
|
||||
) -> None:
|
||||
gateway = TelegramGateway()
|
||||
attempts: list[tuple[str, dict]] = []
|
||||
send_results = iter([primary_failure, {"ok": True}])
|
||||
send_results = iter([primary_failure, dict(_SENT_RECEIPT)])
|
||||
|
||||
async def structured_primary_failure(method: str, payload: dict) -> dict:
|
||||
attempts.append((method, payload))
|
||||
@@ -423,7 +455,7 @@ async def test_approval_structured_no_send_retries_plain_on_exact_parent(
|
||||
return {"ok": True}
|
||||
|
||||
record_failure = AsyncMock(return_value=None)
|
||||
shared_fallback = AsyncMock(return_value={"ok": True})
|
||||
shared_fallback = AsyncMock(return_value=dict(_SENT_RECEIPT))
|
||||
monkeypatch.setattr(gateway, "_send_request", structured_primary_failure)
|
||||
monkeypatch.setattr(gateway, "_record_callback_reply_failure", record_failure)
|
||||
monkeypatch.setattr(gateway, "send_alert_notification", shared_fallback)
|
||||
@@ -454,10 +486,12 @@ async def test_approval_structured_double_no_send_records_durable_failure(
|
||||
) -> None:
|
||||
gateway = TelegramGateway()
|
||||
attempts: list[tuple[str, dict]] = []
|
||||
send_results = iter([
|
||||
{"ok": True, "_awooop_delivery_status": "blocked_no_egress"},
|
||||
{"ok": True, "_awooop_provider_send_performed": False},
|
||||
])
|
||||
send_results = iter(
|
||||
[
|
||||
{"ok": True, "_awooop_delivery_status": "blocked_no_egress"},
|
||||
{"ok": True, "_awooop_provider_send_performed": False},
|
||||
]
|
||||
)
|
||||
|
||||
async def structured_no_send(method: str, payload: dict) -> dict:
|
||||
attempts.append((method, payload))
|
||||
@@ -501,7 +535,7 @@ async def test_callback_card_edits_use_only_verified_inbound_identity(
|
||||
action: str,
|
||||
) -> None:
|
||||
gateway = TelegramGateway()
|
||||
send_request = AsyncMock(return_value={"ok": True})
|
||||
send_request = AsyncMock(return_value=dict(_SENT_RECEIPT))
|
||||
monkeypatch.setattr(gateway, "_send_request", send_request)
|
||||
|
||||
await gateway._update_message_after_action(
|
||||
@@ -658,17 +692,20 @@ async def test_autonomous_lifecycle_threads_use_canonical_route_and_parent_recei
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
gateway = TelegramGateway()
|
||||
send_request = AsyncMock(return_value={"ok": True})
|
||||
send_request = AsyncMock(return_value=dict(_SENT_RECEIPT))
|
||||
monkeypatch.setattr(gateway, "_send_request", send_request)
|
||||
monkeypatch.setattr(gateway_module, "get_redis", lambda: _LifecycleRedis())
|
||||
|
||||
assert await gateway.mark_auto_repaired("INC-1", "playbook", 10) is True
|
||||
assert await gateway.append_incident_update("INC-2", "healthy") is True
|
||||
assert await gateway.append_grouped_alert_digest(
|
||||
incident_id="INC-3",
|
||||
group_key="group-1",
|
||||
digest_text="digest",
|
||||
) is True
|
||||
assert (
|
||||
await gateway.append_grouped_alert_digest(
|
||||
incident_id="INC-3",
|
||||
group_key="group-1",
|
||||
digest_text="digest",
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
send_payloads = [
|
||||
call.args[1]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import runpy
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
@@ -11,13 +13,24 @@ 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}}
|
||||
return {
|
||||
"ok": True,
|
||||
"result": {
|
||||
"message_id": 11,
|
||||
"chat": {"id": self._payload.get("chat_id")},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class _FakeHttpClient:
|
||||
@@ -26,10 +39,12 @@ class _FakeHttpClient:
|
||||
|
||||
async def post(self, url: str, json: dict) -> _FakeResponse:
|
||||
self.posts.append((url, json))
|
||||
return _FakeResponse()
|
||||
return _FakeResponse(json)
|
||||
|
||||
|
||||
def _prepared_gateway(monkeypatch: pytest.MonkeyPatch) -> tuple[TelegramGateway, _FakeHttpClient]:
|
||||
def _prepared_gateway(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> tuple[TelegramGateway, _FakeHttpClient]:
|
||||
gateway = TelegramGateway()
|
||||
client = _FakeHttpClient()
|
||||
gateway._initialized = True
|
||||
@@ -115,6 +130,77 @@ async def test_notification_provider_requires_affirmative_delivery_ack(
|
||||
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,
|
||||
@@ -131,6 +217,92 @@ async def test_unclassified_send_message_is_blocked_before_provider(
|
||||
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,
|
||||
@@ -209,15 +381,77 @@ async def test_allowed_legacy_monitoring_methods_use_canonical_provider_once(
|
||||
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",
|
||||
@@ -235,6 +469,100 @@ async def test_openclaw_cannot_own_tsenyang_monitoring_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,
|
||||
|
||||
@@ -13,6 +13,7 @@ from src.models.incident import Incident, Severity, Signal
|
||||
from src.repositories.alert_operation_log_repository import ALERT_EVENT_TYPES
|
||||
from src.services import decision_manager as decision_module
|
||||
from src.services import report_generation_service as report_module
|
||||
from src.services import telegram_gateway as gateway_module
|
||||
from src.services.ai_rate_limiter import AIRateLimiter
|
||||
from src.services.approval_execution import ApprovalExecutionService
|
||||
from src.services.report_generation_service import ReportGenerationService
|
||||
@@ -27,11 +28,33 @@ NO_SEND_RECEIPT = {
|
||||
},
|
||||
}
|
||||
|
||||
_SENT_CHAT_ID = -1001
|
||||
_SENT_DESTINATION_BINDING = gateway_module._telegram_destination_binding(
|
||||
_SENT_CHAT_ID
|
||||
)
|
||||
SENT_RECEIPT = {
|
||||
"ok": True,
|
||||
"_awooop_delivery_status": "sent",
|
||||
"_awooop_provider_send_performed": True,
|
||||
"result": {"message_id": 42, "chat": {"id": -1001}},
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
"schema_version": "telegram_canonical_egress_receipt_v1",
|
||||
"decision": "allowed",
|
||||
"destination_alias": "awoooi_sre_war_room",
|
||||
"destination_binding": _SENT_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": _SENT_DESTINATION_BINDING,
|
||||
"payload_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"provider_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"destination_binding_verified": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -260,9 +283,7 @@ async def test_approval_blocked_receipt_writes_no_send_not_sent(monkeypatch) ->
|
||||
repair_attempted=True,
|
||||
)
|
||||
|
||||
assert [row["event_type"] for row in operations] == [
|
||||
"NOTIFICATION_CLASSIFIED"
|
||||
]
|
||||
assert [row["event_type"] for row in operations] == ["NOTIFICATION_CLASSIFIED"]
|
||||
assert operations[0]["event_type"] in ALERT_EVENT_TYPES
|
||||
assert operations[0]["action_detail"] == "telegram_execution_result_no_send"
|
||||
assert operations[0]["success"] is False
|
||||
|
||||
@@ -24,6 +24,7 @@ from src.services import converged_alert_recurrence_notifier as recurrence_modul
|
||||
from src.services import decision_manager as decision_module
|
||||
from src.services import failover_alerter as failover_module
|
||||
from src.services import k3s_monitor_service as k3s_module
|
||||
from src.services import telegram_gateway as gateway_module
|
||||
from src.services import weekly_report_service as weekly_module
|
||||
|
||||
NO_PROVIDER_SEND_RECEIPT = {
|
||||
@@ -36,11 +37,33 @@ NO_PROVIDER_SEND_RECEIPT = {
|
||||
},
|
||||
}
|
||||
|
||||
_SENT_CHAT_ID = -1001
|
||||
_SENT_DESTINATION_BINDING = gateway_module._telegram_destination_binding(
|
||||
_SENT_CHAT_ID
|
||||
)
|
||||
SENT_RECEIPT = {
|
||||
"ok": True,
|
||||
"_awooop_delivery_status": "sent",
|
||||
"_awooop_provider_send_performed": True,
|
||||
"result": {"message_id": 42, "chat": {"id": -1001}},
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
"schema_version": "telegram_canonical_egress_receipt_v1",
|
||||
"decision": "allowed",
|
||||
"destination_alias": "awoooi_sre_war_room",
|
||||
"destination_binding": _SENT_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": _SENT_DESTINATION_BINDING,
|
||||
"payload_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"provider_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"destination_binding_verified": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +193,9 @@ async def test_watchdog_releases_dedup_and_does_not_log_sent_without_provider_ac
|
||||
logger = _Logger()
|
||||
monkeypatch.setattr(watchdog_module, "logger", logger)
|
||||
monkeypatch.setattr(watchdog_module, "get_redis", lambda: redis)
|
||||
monkeypatch.setattr(watchdog_module, "_is_grace_active", AsyncMock(return_value=True))
|
||||
monkeypatch.setattr(
|
||||
watchdog_module, "_is_grace_active", AsyncMock(return_value=True)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
watchdog_module,
|
||||
"_count_pending_no_tg_sent",
|
||||
|
||||
@@ -25,11 +25,33 @@ NO_SEND_RECEIPT = {
|
||||
},
|
||||
}
|
||||
|
||||
_SENT_CHAT_ID = -1001
|
||||
_SENT_DESTINATION_BINDING = gateway_module._telegram_destination_binding(
|
||||
_SENT_CHAT_ID
|
||||
)
|
||||
SENT_RECEIPT = {
|
||||
"ok": True,
|
||||
"_awooop_delivery_status": "sent",
|
||||
"_awooop_provider_send_performed": True,
|
||||
"result": {"message_id": 42},
|
||||
"result": {"message_id": 42, "chat": {"id": _SENT_CHAT_ID}},
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
"schema_version": "telegram_canonical_egress_receipt_v1",
|
||||
"decision": "allowed",
|
||||
"destination_alias": "awoooi_sre_war_room",
|
||||
"destination_binding": _SENT_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": _SENT_DESTINATION_BINDING,
|
||||
"payload_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"provider_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"destination_binding_verified": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -73,9 +95,86 @@ class FakeRedis:
|
||||
return True
|
||||
|
||||
|
||||
def test_delivery_helper_rejects_truthy_structured_no_send() -> None:
|
||||
assert _telegram_send_delivery_succeeded(dict(NO_SEND_RECEIPT)) is False
|
||||
assert _telegram_send_delivery_succeeded({"ok": True}) is True
|
||||
@pytest.mark.parametrize(
|
||||
("receipt", "expected"),
|
||||
[
|
||||
(SENT_RECEIPT, True),
|
||||
(NO_SEND_RECEIPT, False),
|
||||
({"ok": True}, False),
|
||||
({**SENT_RECEIPT, "ok": 1}, False),
|
||||
({**SENT_RECEIPT, "_awooop_delivery_status": ""}, False),
|
||||
({**SENT_RECEIPT, "_awooop_provider_send_performed": None}, False),
|
||||
({**SENT_RECEIPT, "result": {}}, False),
|
||||
({**SENT_RECEIPT, "result": {"message_id": 0}}, False),
|
||||
({**SENT_RECEIPT, "result": {"message_id": True}}, False),
|
||||
({**SENT_RECEIPT, "result": {"message_id": "42"}}, False),
|
||||
({**SENT_RECEIPT, "_awoooi_canonical_route_receipt": {}}, False),
|
||||
({**SENT_RECEIPT, gateway_module._DELIVERY_CONTEXT_KEY: {}}, False),
|
||||
(
|
||||
{
|
||||
**SENT_RECEIPT,
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
"schema_version": "telegram_canonical_egress_receipt_v1",
|
||||
"decision": "blocked_no_egress",
|
||||
"provider_send_performed": True,
|
||||
},
|
||||
},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
**SENT_RECEIPT,
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
**SENT_RECEIPT["_awoooi_canonical_route_receipt"],
|
||||
"sender_bot_alias": "",
|
||||
},
|
||||
},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
**SENT_RECEIPT,
|
||||
gateway_module._DELIVERY_CONTEXT_KEY: {
|
||||
**SENT_RECEIPT[gateway_module._DELIVERY_CONTEXT_KEY],
|
||||
"sender_bot_alias": "openclaw_bot",
|
||||
},
|
||||
},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
**SENT_RECEIPT,
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
**SENT_RECEIPT["_awoooi_canonical_route_receipt"],
|
||||
"destination_alias": "other_destination",
|
||||
},
|
||||
},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
**SENT_RECEIPT,
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
**SENT_RECEIPT["_awoooi_canonical_route_receipt"],
|
||||
"destination_binding": "",
|
||||
},
|
||||
},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
**SENT_RECEIPT,
|
||||
"result": {"message_id": 42, "chat": {"id": -2002}},
|
||||
},
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_delivery_helper_fails_closed_without_complete_canonical_receipt(
|
||||
receipt: object,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
assert _telegram_send_delivery_succeeded(receipt) is expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -244,7 +343,8 @@ async def test_html_callback_no_send_exhausts_fallback_and_records_failure(
|
||||
assert send_request.await_count == 3
|
||||
record_failure.assert_awaited_once()
|
||||
statuses = [
|
||||
call.args[1].get(gateway_module._AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY, {})
|
||||
call.args[1]
|
||||
.get(gateway_module._AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY, {})
|
||||
.get("callback_reply", {})
|
||||
.get("status")
|
||||
for call in send_request.await_args_list
|
||||
@@ -298,6 +398,7 @@ async def test_interactive_reply_no_send_logs_failure_not_sent(
|
||||
failed_event = "ai_advisory_group_reply_failed"
|
||||
sent_event = "ai_advisory_group_reply_sent"
|
||||
else:
|
||||
|
||||
class Repo:
|
||||
async def get_by_id(self, _incident_id: str):
|
||||
return None
|
||||
@@ -451,9 +552,7 @@ async def test_dev_test_push_returns_false_for_structured_no_send(
|
||||
monkeypatch.setattr(telegram.settings, "ENVIRONMENT", "dev")
|
||||
monkeypatch.setattr(telegram, "get_telegram_gateway", lambda: Gateway())
|
||||
|
||||
response = await telegram.test_push(
|
||||
telegram.TestPushRequest(approval_id="APR-1")
|
||||
)
|
||||
response = await telegram.test_push(telegram.TestPushRequest(approval_id="APR-1"))
|
||||
|
||||
assert response["ok"] is False
|
||||
assert response["message"] == "Test push no-send"
|
||||
|
||||
@@ -26,6 +26,35 @@ from src.services.telegram_gateway import (
|
||||
normalize_telegram_send_message_payload,
|
||||
)
|
||||
|
||||
_SENT_CHAT_ID = -1001
|
||||
_SENT_DESTINATION_BINDING = (
|
||||
telegram_gateway_module._telegram_destination_binding(_SENT_CHAT_ID)
|
||||
)
|
||||
_CANONICAL_SENT_RECEIPT = {
|
||||
"ok": True,
|
||||
"_awooop_delivery_status": "sent",
|
||||
"_awooop_provider_send_performed": True,
|
||||
"result": {"message_id": 77, "chat": {"id": _SENT_CHAT_ID}},
|
||||
"_awoooi_canonical_route_receipt": {
|
||||
"schema_version": "telegram_canonical_egress_receipt_v1",
|
||||
"decision": "allowed",
|
||||
"destination_alias": "awoooi_sre_war_room",
|
||||
"destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"sender_bot_alias": "tsenyang_bot",
|
||||
"provider_send_performed": True,
|
||||
},
|
||||
telegram_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": _SENT_DESTINATION_BINDING,
|
||||
"payload_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"provider_destination_binding": _SENT_DESTINATION_BINDING,
|
||||
"destination_binding_verified": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_auto_repair_status_line_distinguishes_ai_retry_queued() -> None:
|
||||
"""自動化失敗 reply 必須明確標示 AI 續跑,且不把 raw error 當純文字噴出。"""
|
||||
@@ -289,7 +318,7 @@ async def test_send_alert_notification_normalizes_host_resource_raw_dump(monkeyp
|
||||
|
||||
async def fake_send_request(method, payload):
|
||||
sent_requests.append((method, payload))
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(TelegramGateway, "alert_chat_id", property(lambda _self: "chat"))
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
@@ -329,7 +358,7 @@ async def test_send_alert_notification_normalizes_aiops_signal_alert(monkeypatch
|
||||
|
||||
async def fake_send_request(method, payload):
|
||||
sent_requests.append((method, payload))
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(TelegramGateway, "alert_chat_id", property(lambda _self: "chat"))
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
@@ -384,7 +413,7 @@ async def test_send_alert_notification_forces_html_card_for_markdown_host_alert(
|
||||
|
||||
async def fake_send_request(method, payload):
|
||||
sent_requests.append((method, payload))
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(TelegramGateway, "alert_chat_id", property(lambda _self: "chat"))
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
@@ -417,7 +446,7 @@ async def test_send_text_normalizes_host_resource_alert(monkeypatch) -> None:
|
||||
|
||||
async def fake_send_request(method, payload):
|
||||
sent_requests.append((method, payload))
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(TelegramGateway, "alert_chat_id", property(lambda _self: "chat"))
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
@@ -482,7 +511,10 @@ async def test_send_request_mirrors_direct_ai_alert_card_to_agent99(monkeypatch)
|
||||
return None
|
||||
|
||||
def json(self) -> dict:
|
||||
return {"ok": True, "result": {"message_id": 991}}
|
||||
return {
|
||||
"ok": True,
|
||||
"result": {"message_id": 991, "chat": {"id": "chat"}},
|
||||
}
|
||||
|
||||
class FakeHttpClient:
|
||||
async def post(self, url, json): # type: ignore[no-untyped-def]
|
||||
@@ -541,7 +573,10 @@ async def test_send_request_mirrors_direct_ai_alert_card_to_agent99(monkeypatch)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["result"] == {"message_id": 991}
|
||||
assert result["result"] == {
|
||||
"message_id": 991,
|
||||
"chat": {"id": "chat"},
|
||||
}
|
||||
assert result["_awoooi_canonical_route_receipt"]["decision"] == "allowed"
|
||||
assert mirrored
|
||||
assert mirrored[0]["source"] == "send_request"
|
||||
@@ -1789,7 +1824,10 @@ async def test_send_request_strips_awooop_callback_metadata_before_telegram_api(
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"ok": True, "result": {"message_id": 456}}
|
||||
return {
|
||||
"ok": True,
|
||||
"result": {"message_id": 456, "chat": {"id": "chat"}},
|
||||
}
|
||||
|
||||
class FakeClient:
|
||||
async def post(self, url, json):
|
||||
@@ -1878,7 +1916,7 @@ async def test_send_html_line_message_falls_back_to_plain_text_on_parse_error(mo
|
||||
sent_requests.append((method, payload))
|
||||
if payload.get("parse_mode") == "HTML":
|
||||
raise telegram_gateway_module.TelegramGatewayError("HTTP error: 400")
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
|
||||
@@ -1910,7 +1948,7 @@ async def test_send_html_line_message_marks_callback_reply_evidence(monkeypatch)
|
||||
sent_requests.append((method, payload))
|
||||
if payload.get("parse_mode") == "HTML":
|
||||
raise telegram_gateway_module.TelegramGatewayError("HTTP error: 400")
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
|
||||
@@ -2002,7 +2040,13 @@ async def test_send_html_line_message_marks_callback_reply_evidence(monkeypatch)
|
||||
acknowledged = (
|
||||
telegram_gateway_module._acknowledge_callback_reply_source_envelope(
|
||||
fallback_source_extra,
|
||||
delivery_result={"ok": True},
|
||||
delivery_result={
|
||||
**_CANONICAL_SENT_RECEIPT,
|
||||
"result": {
|
||||
"message_id": 88,
|
||||
"chat": {"id": _SENT_CHAT_ID},
|
||||
},
|
||||
},
|
||||
provider_message_id="88",
|
||||
)
|
||||
)
|
||||
@@ -2042,7 +2086,7 @@ async def test_send_html_line_message_uses_rescue_when_markup_fallback_fails(mon
|
||||
sent_requests.append((method, payload))
|
||||
if len(sent_requests) < 3:
|
||||
raise telegram_gateway_module.TelegramGatewayError("HTTP error: 400")
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
|
||||
@@ -2070,7 +2114,7 @@ async def test_send_html_line_message_attaches_awooop_markup_to_first_chunk(monk
|
||||
|
||||
async def fake_send_request(method, payload):
|
||||
sent_requests.append((method, payload))
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
|
||||
@@ -2160,7 +2204,7 @@ async def test_send_notification_long_html_uses_plain_text_without_cutting_tags(
|
||||
|
||||
async def fake_send_request(method, payload):
|
||||
sent_requests.append((method, payload))
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
long_text = "📊 <b>事件歷史統計</b>\n告警鍵: <code>" + ("node<188>&" * 90) + "</code>"
|
||||
@@ -2446,7 +2490,7 @@ async def test_append_incident_update_deduplicates_same_status(monkeypatch):
|
||||
|
||||
async def fake_send_request(method, payload):
|
||||
sent_requests.append((method, payload))
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(telegram_gateway_module, "get_redis", lambda: fake_redis)
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
@@ -2491,7 +2535,7 @@ async def test_append_incident_update_suppresses_duplicate_failure_across_incide
|
||||
|
||||
async def fake_send_request(method, payload):
|
||||
sent_requests.append((method, payload))
|
||||
return {"ok": True}
|
||||
return dict(_CANONICAL_SENT_RECEIPT)
|
||||
|
||||
monkeypatch.setattr(telegram_gateway_module, "get_redis", lambda: fake_redis)
|
||||
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
|
||||
|
||||
495
apps/api/tests/test_telegram_notification_egress_scanners.py
Normal file
495
apps/api/tests/test_telegram_notification_egress_scanners.py
Normal file
@@ -0,0 +1,495 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def _load_scanner(filename: str, module_name: str) -> ModuleType:
|
||||
path = ROOT / "scripts/security" / filename
|
||||
spec = importlib.util.spec_from_file_location(module_name, path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
INVENTORY = _load_scanner(
|
||||
"telegram-notification-egress-inventory.py",
|
||||
"telegram_notification_egress_inventory_test",
|
||||
)
|
||||
NO_NEW_BYPASS_GUARD = _load_scanner(
|
||||
"telegram-notification-egress-no-new-bypass-guard.py",
|
||||
"telegram_notification_egress_no_new_bypass_guard_test",
|
||||
)
|
||||
SCANNERS = (INVENTORY, NO_NEW_BYPASS_GUARD)
|
||||
|
||||
|
||||
def _write_zero_baseline(root: Path) -> None:
|
||||
snapshot = (
|
||||
root / "docs/security/telegram-notification-egress-inventory.snapshot.json"
|
||||
)
|
||||
snapshot.parent.mkdir(parents=True, exist_ok=True)
|
||||
snapshot.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"summary": {
|
||||
"direct_bot_api_call_count": 0,
|
||||
"direct_bot_api_file_count": 0,
|
||||
},
|
||||
"direct_bot_api_calls": [],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _write_minimal_gateway(root: Path) -> None:
|
||||
gateway = root / "apps/api/src/services/telegram_gateway.py"
|
||||
gateway.parent.mkdir(parents=True, exist_ok=True)
|
||||
gateway.write_text(
|
||||
"normalize_telegram_send_message_payload = object()\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
def test_scanner_detects_multiline_split_bot_api_url(scanner: ModuleType) -> None:
|
||||
source = """
|
||||
import requests
|
||||
|
||||
def send_telegram_alert(token: str) -> object:
|
||||
base = "https://api.telegram.org/"
|
||||
endpoint = (
|
||||
base
|
||||
+ f"bot{token}/"
|
||||
+ "sendMessage"
|
||||
)
|
||||
return requests.post(endpoint, json={"text": "bounded"})
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces("scripts/ops/split.py", source)
|
||||
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["method"] == "sendMessage"
|
||||
assert findings[0]["detection_kind"] == "direct_bot_api_endpoint"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
@pytest.mark.parametrize("transport", ["Invoke-WebRequest", "Invoke-RestMethod"])
|
||||
def test_scanner_detects_powershell_direct_transport(
|
||||
scanner: ModuleType,
|
||||
transport: str,
|
||||
) -> None:
|
||||
source = f"""
|
||||
function Send-AgentTelegramLegacy {{
|
||||
$base = "https://api.telegram.org/"
|
||||
$uri = $base + "bot" + $Token + "/sendPhoto"
|
||||
{transport} -Method Post -Uri $uri
|
||||
}}
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"agent99-control-plane.ps1",
|
||||
source,
|
||||
)
|
||||
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["method"] == "sendPhoto"
|
||||
assert findings[0]["detection_kind"] == "direct_bot_api_endpoint"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
@pytest.mark.parametrize(
|
||||
"transport",
|
||||
[
|
||||
"requests.post(target, json=payload)",
|
||||
"urllib.request.urlopen(target, data=payload)",
|
||||
"client.PostAsync(target, content)",
|
||||
],
|
||||
)
|
||||
def test_scanner_detects_custom_sender_without_literal_endpoint(
|
||||
scanner: ModuleType,
|
||||
transport: str,
|
||||
) -> None:
|
||||
source = f"""
|
||||
function Send-AgentTelegramLegacy {{
|
||||
$target = $ConfiguredProviderUrl + "/sendMessage"
|
||||
{transport}
|
||||
}}
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"agent99-control-plane.ps1",
|
||||
source,
|
||||
)
|
||||
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["method"] == "sendMessage"
|
||||
assert findings[0]["detection_kind"] == "custom_direct_sender"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
@pytest.mark.parametrize(
|
||||
"transport",
|
||||
[
|
||||
"httpx.Client().post(endpoint, json=payload)",
|
||||
"requests.Session().post(endpoint, json=payload)",
|
||||
"urllib.request.build_opener().open(endpoint, data=payload)",
|
||||
"opener.open(endpoint, data=payload)",
|
||||
],
|
||||
)
|
||||
def test_scanner_detects_factory_clients_and_urllib_openers_with_dynamic_endpoint(
|
||||
scanner: ModuleType,
|
||||
transport: str,
|
||||
) -> None:
|
||||
source = f"""
|
||||
def dispatch(endpoint: str, bot_token: str, chat_id: str, payload: bytes) -> object:
|
||||
return {transport}
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"scripts/ops/provider_dispatch.py",
|
||||
source,
|
||||
)
|
||||
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["method"] == "dynamic"
|
||||
assert findings[0]["detection_kind"] == "custom_direct_sender"
|
||||
assert findings[0]["function"] == "dispatch"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
@pytest.mark.parametrize(
|
||||
("imports", "transport"),
|
||||
[
|
||||
("import requests as rq", "rq.post(endpoint, json=payload)"),
|
||||
("import httpx as hx", "hx.post(endpoint, json=payload)"),
|
||||
(
|
||||
"import httpx as hx",
|
||||
"hx.Client().post(endpoint, json=payload)",
|
||||
),
|
||||
(
|
||||
"from httpx import Client as HC",
|
||||
"HC().post(endpoint, json=payload)",
|
||||
),
|
||||
(
|
||||
"from requests import Session as S",
|
||||
"S().post(endpoint, json=payload)",
|
||||
),
|
||||
(
|
||||
"from urllib.request import build_opener as make_opener",
|
||||
"make_opener().open(endpoint, data=payload)",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_scanner_resolves_python_import_alias_transport_calls(
|
||||
scanner: ModuleType,
|
||||
imports: str,
|
||||
transport: str,
|
||||
) -> None:
|
||||
source = f"""
|
||||
{imports}
|
||||
|
||||
def dispatch(endpoint: str, bot_token: str, chat_id: str, payload: bytes) -> object:
|
||||
return {transport}
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"scripts/ops/aliased_provider_dispatch.py",
|
||||
source,
|
||||
)
|
||||
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["method"] == "dynamic"
|
||||
assert findings[0]["detection_kind"] == "custom_direct_sender"
|
||||
assert findings[0]["function"] == "dispatch"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
@pytest.mark.parametrize(
|
||||
("imports", "transport"),
|
||||
[
|
||||
("import requests as rq", "rq.post(endpoint, json=payload)"),
|
||||
(
|
||||
"import httpx as hx",
|
||||
"hx.Client().post(endpoint, json=payload)",
|
||||
),
|
||||
(
|
||||
"from httpx import Client as HC",
|
||||
"HC().post(endpoint, json=payload)",
|
||||
),
|
||||
(
|
||||
"from requests import Session as S",
|
||||
"S().post(endpoint, json=payload)",
|
||||
),
|
||||
(
|
||||
"from urllib.request import build_opener as make_opener",
|
||||
"make_opener().open(endpoint, data=payload)",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_scanner_resolves_module_scope_import_alias_transport_calls(
|
||||
scanner: ModuleType,
|
||||
imports: str,
|
||||
transport: str,
|
||||
) -> None:
|
||||
source = f"""
|
||||
{imports}
|
||||
|
||||
bot_token = object()
|
||||
chat_id = object()
|
||||
endpoint = object()
|
||||
payload = b""
|
||||
result = {transport}
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"scripts/ops/module_alias_provider.py",
|
||||
source,
|
||||
)
|
||||
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["method"] == "dynamic"
|
||||
assert findings[0]["detection_kind"] == "custom_direct_sender"
|
||||
assert findings[0]["function"] == "<module>"
|
||||
assert findings[0]["function_qualified"] == "<module>"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
def test_scanner_resolves_imported_factory_assigned_to_local_client(
|
||||
scanner: ModuleType,
|
||||
) -> None:
|
||||
source = """
|
||||
from httpx import Client as HC
|
||||
|
||||
def dispatch(endpoint: str, bot_token: str, chat_id: str, payload: bytes) -> object:
|
||||
sender = HC()
|
||||
return sender.post(endpoint, content=payload)
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"scripts/ops/assigned_alias_provider.py",
|
||||
source,
|
||||
)
|
||||
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["method"] == "dynamic"
|
||||
assert findings[0]["function"] == "dispatch"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
@pytest.mark.parametrize("upload_method", ["UploadString", "UploadData", "UploadFile"])
|
||||
def test_scanner_detects_powershell_webclient_uploads_with_dynamic_endpoint(
|
||||
scanner: ModuleType,
|
||||
upload_method: str,
|
||||
) -> None:
|
||||
source = f"""
|
||||
function Publish-LegacyPayload {{
|
||||
param($BotToken, $ChatId, $Uri)
|
||||
$webClient = New-Object System.Net.WebClient
|
||||
$webClient.{upload_method}($Uri, $Payload)
|
||||
}}
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"scripts/ops/legacy-provider.ps1",
|
||||
source,
|
||||
)
|
||||
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["method"] == "dynamic"
|
||||
assert findings[0]["detection_kind"] == "custom_direct_sender"
|
||||
assert findings[0]["function"] == "Publish-LegacyPayload"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
def test_scanner_excludes_only_canonical_gateway_final_exit(
|
||||
scanner: ModuleType,
|
||||
) -> None:
|
||||
source = """
|
||||
class TelegramGateway:
|
||||
async def _send_request(self, token: str, payload: dict) -> object:
|
||||
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
||||
return await self._http_client.post(url, json=payload)
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"apps/api/src/services/telegram_gateway.py",
|
||||
source,
|
||||
)
|
||||
|
||||
assert findings == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
def test_scanner_does_not_allowlist_same_function_name_outside_gateway(
|
||||
scanner: ModuleType,
|
||||
) -> None:
|
||||
source = """
|
||||
import requests as rq
|
||||
|
||||
async def _send_request(
|
||||
endpoint: str,
|
||||
bot_token: str,
|
||||
chat_id: str,
|
||||
payload: dict,
|
||||
) -> object:
|
||||
return rq.post(endpoint, json=payload)
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"scripts/ops/telegram_gateway.py",
|
||||
source,
|
||||
)
|
||||
|
||||
assert len(findings) == 1
|
||||
assert findings[0]["function"] == "_send_request"
|
||||
assert findings[0]["detection_kind"] == "custom_direct_sender"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scanner", SCANNERS)
|
||||
def test_scanner_allowlists_only_class_qualified_gateway_final_exit(
|
||||
scanner: ModuleType,
|
||||
) -> None:
|
||||
source = """
|
||||
import requests as rq
|
||||
|
||||
async def _send_request(
|
||||
endpoint: str,
|
||||
bot_token: str,
|
||||
chat_id: str,
|
||||
payload: dict,
|
||||
) -> object:
|
||||
return rq.post(endpoint, json=payload)
|
||||
|
||||
class RogueGateway:
|
||||
async def _send_request(
|
||||
self,
|
||||
endpoint: str,
|
||||
bot_token: str,
|
||||
chat_id: str,
|
||||
payload: dict,
|
||||
) -> object:
|
||||
return rq.post(endpoint, json=payload)
|
||||
|
||||
class TelegramGateway:
|
||||
async def _send_request(
|
||||
self,
|
||||
endpoint: str,
|
||||
bot_token: str,
|
||||
chat_id: str,
|
||||
payload: dict,
|
||||
) -> object:
|
||||
return rq.post(endpoint, json=payload)
|
||||
"""
|
||||
|
||||
findings = scanner.scan_direct_bot_api_surfaces(
|
||||
"apps/api/src/services/telegram_gateway.py",
|
||||
source,
|
||||
)
|
||||
|
||||
assert {item["function_qualified"] for item in findings} == {
|
||||
"_send_request",
|
||||
"RogueGateway._send_request",
|
||||
}
|
||||
|
||||
|
||||
def test_no_new_bypass_guard_fails_closed_on_split_endpoint(tmp_path: Path) -> None:
|
||||
_write_zero_baseline(tmp_path)
|
||||
_write_minimal_gateway(tmp_path)
|
||||
sender = tmp_path / "scripts/ops/split_sender.py"
|
||||
sender.parent.mkdir(parents=True, exist_ok=True)
|
||||
sender.write_text(
|
||||
"""
|
||||
import requests
|
||||
|
||||
def send_telegram(token: str) -> object:
|
||||
base = "https://api.telegram.org/"
|
||||
endpoint = base + f"bot{token}/" + "sendDocument"
|
||||
return requests.post(endpoint, files={})
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
report = NO_NEW_BYPASS_GUARD.build_report(tmp_path)
|
||||
|
||||
assert report["summary"]["current_direct_bot_api_call_count"] == 1
|
||||
assert report["summary"]["new_bypass_count"] == 1
|
||||
with pytest.raises(SystemExit, match="direct/custom bypass"):
|
||||
NO_NEW_BYPASS_GUARD.validate(tmp_path)
|
||||
|
||||
|
||||
def test_local_github_workflow_is_frozen_legacy_not_active_bypass(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
_write_zero_baseline(tmp_path)
|
||||
_write_minimal_gateway(tmp_path)
|
||||
workflow = tmp_path / ".github/workflows/legacy-alert.yml"
|
||||
workflow.parent.mkdir(parents=True, exist_ok=True)
|
||||
workflow.write_text(
|
||||
"""
|
||||
steps:
|
||||
- name: frozen legacy telegram sender
|
||||
run: |
|
||||
endpoint="https://api.telegram.org/bot${BOT_TOKEN}/sendMessage"
|
||||
curl -X POST "$endpoint" -d "chat_id=${CHAT_ID}"
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
inventory_report = INVENTORY.build_report(tmp_path)
|
||||
inventory_item = inventory_report["direct_bot_api_calls"][0]
|
||||
assert inventory_report["summary"]["direct_bot_api_call_count"] == 1
|
||||
assert inventory_report["summary"]["active_direct_bot_api_call_count"] == 0
|
||||
assert (
|
||||
inventory_report["summary"]["github_frozen_legacy_direct_bot_api_call_count"]
|
||||
== 1
|
||||
)
|
||||
assert inventory_item["source_truth_classification"] == (
|
||||
"frozen_legacy_source_truth"
|
||||
)
|
||||
assert inventory_item["runtime_execution_authorized"] is False
|
||||
assert inventory_item["workflow_execution_authorized"] is False
|
||||
assert (
|
||||
inventory_report["execution_boundaries"]["github_workflow_execution_authorized"]
|
||||
is False
|
||||
)
|
||||
|
||||
guard_report = NO_NEW_BYPASS_GUARD.build_report(tmp_path)
|
||||
assert guard_report["status"] == "pass_no_direct_or_custom_bypass"
|
||||
assert guard_report["summary"]["detected_direct_bot_api_call_count"] == 1
|
||||
assert guard_report["summary"]["current_direct_bot_api_call_count"] == 0
|
||||
assert guard_report["summary"]["new_bypass_count"] == 0
|
||||
assert (
|
||||
guard_report["summary"]["github_frozen_legacy_direct_bot_api_call_count"] == 1
|
||||
)
|
||||
frozen_item = guard_report["frozen_legacy_direct_bot_api_calls"][0]
|
||||
assert frozen_item["source_truth_classification"] == ("frozen_legacy_source_truth")
|
||||
assert frozen_item["runtime_execution_authorized"] is False
|
||||
assert frozen_item["workflow_execution_authorized"] is False
|
||||
assert (
|
||||
guard_report["execution_boundaries"]["github_workflow_execution_authorized"]
|
||||
is False
|
||||
)
|
||||
NO_NEW_BYPASS_GUARD.validate(tmp_path)
|
||||
|
||||
|
||||
def test_agent99_has_no_direct_or_custom_telegram_sender_source() -> None:
|
||||
source = (ROOT / "agent99-control-plane.ps1").read_text(encoding="utf-8")
|
||||
|
||||
for scanner in SCANNERS:
|
||||
assert (
|
||||
scanner.scan_direct_bot_api_surfaces("agent99-control-plane.ps1", source)
|
||||
== []
|
||||
)
|
||||
assert "function Send-AgentTelegramRelay" not in source
|
||||
assert "function Send-AgentTelegramPhotoDirect" not in source
|
||||
assert "canonical_telegram_gateway_transport_required" in source
|
||||
assert "providerSendPerformed = $false" in source
|
||||
assert 'routeStatus = "blocked_no_egress"' in source
|
||||
Reference in New Issue
Block a user