fix(agent99): route lifecycle alerts through canonical gateway
This commit is contained in:
208
apps/api/tests/test_agent99_telegram_lifecycle_api.py
Normal file
208
apps/api/tests/test_agent99_telegram_lifecycle_api.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# ruff: noqa: E402
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import ValidationError
|
||||
|
||||
from src.api.v1 import agents as agents_api
|
||||
from src.core.config import settings
|
||||
from src.models.agent99_completion import Agent99TelegramLifecycleRequest
|
||||
from src.services import agent99_telegram_lifecycle as lifecycle_service
|
||||
from src.services import telegram_gateway as gateway_service
|
||||
|
||||
|
||||
def payload() -> dict:
|
||||
return {
|
||||
"schema_version": "agent99_telegram_lifecycle_v1",
|
||||
"delivery_id": "agent99-lifecycle-0123456789abcdef0123456789abcdef01234567",
|
||||
"project_id": "awoooi",
|
||||
"incident_id": "INC-20260715-AG9901",
|
||||
"fingerprint": "0123456789abcdef0123456789abcdef",
|
||||
"event_type": "performance_warning",
|
||||
"severity": "warning",
|
||||
"lifecycle": "investigating",
|
||||
"state_key": "investigating|warning",
|
||||
"run_id": "run-agent99-20260715-01",
|
||||
"title": "Host 110 主機效能異常",
|
||||
"target": "192.168.0.110",
|
||||
"impact": "資源壓力可能降低服務回應速度。",
|
||||
"action": "Agent99 正在執行 allowlisted 診斷與降載。",
|
||||
"verification": "等待 CPU、記憶體與磁碟 post-verifier。",
|
||||
"duration_text": "12.4 秒",
|
||||
"occurrences": 2,
|
||||
"next_update": "狀態改變時更新,相同狀態不重複推播。",
|
||||
"occurred_at": "2026-07-15T04:20:00+08:00",
|
||||
}
|
||||
|
||||
|
||||
def app_client() -> TestClient:
|
||||
app = FastAPI()
|
||||
app.include_router(agents_api.router, prefix="/api/v1")
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_lifecycle_payload_rejects_unbounded_content() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
Agent99TelegramLifecycleRequest.model_validate(
|
||||
{**payload(), "raw_log": "must-not-enter-lifecycle-ingress"}
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
Agent99TelegramLifecycleRequest.model_validate(
|
||||
{**payload(), "incident_id": r"C:\Wooo\Agent99\evidence\raw.json"}
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
Agent99TelegramLifecycleRequest.model_validate(
|
||||
{**payload(), "project_id": "unregistered-product"}
|
||||
)
|
||||
|
||||
|
||||
def test_lifecycle_endpoint_rejects_wrong_token(monkeypatch) -> None:
|
||||
monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "expected")
|
||||
|
||||
response = app_client().post(
|
||||
"/api/v1/agents/agent99/telegram-lifecycle",
|
||||
headers={"X-Agent99-Lifecycle-Token": "wrong"},
|
||||
json=payload(),
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "agent99_lifecycle_auth_failed"
|
||||
|
||||
|
||||
def test_lifecycle_endpoint_requires_durable_gateway_ack(monkeypatch) -> None:
|
||||
monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "expected")
|
||||
|
||||
async def failed_delivery(request):
|
||||
return {
|
||||
"ok": False,
|
||||
"delivery_id": request.delivery_id,
|
||||
"delivery_status": "pending_unknown",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
agents_api,
|
||||
"deliver_agent99_telegram_lifecycle",
|
||||
failed_delivery,
|
||||
)
|
||||
response = app_client().post(
|
||||
"/api/v1/agents/agent99/telegram-lifecycle",
|
||||
headers={"X-Agent99-Lifecycle-Token": "expected"},
|
||||
json=payload(),
|
||||
)
|
||||
|
||||
assert response.status_code == 503
|
||||
assert "pending_unknown" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_lifecycle_endpoint_returns_same_delivery_receipt(monkeypatch) -> None:
|
||||
monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "expected")
|
||||
|
||||
async def successful_delivery(request):
|
||||
return {
|
||||
"schema_version": "agent99_telegram_lifecycle_receipt_v1",
|
||||
"ok": True,
|
||||
"delivery_id": request.delivery_id,
|
||||
"incident_id": request.incident_id,
|
||||
"delivery_status": "sent",
|
||||
"provider_message_id": "8123",
|
||||
"durable_outbound_acknowledged": True,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
agents_api,
|
||||
"deliver_agent99_telegram_lifecycle",
|
||||
successful_delivery,
|
||||
)
|
||||
response = app_client().post(
|
||||
"/api/v1/agents/agent99/telegram-lifecycle",
|
||||
headers={"X-Agent99-Lifecycle-Token": "expected"},
|
||||
json=payload(),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["delivery_id"] == payload()["delivery_id"]
|
||||
assert response.json()["provider_message_id"] == "8123"
|
||||
|
||||
|
||||
def test_lifecycle_outbox_identity_is_stable_and_separate() -> None:
|
||||
base = {
|
||||
"callback_reply": {
|
||||
"project_id": "awoooi",
|
||||
"automation_run_id": "agent99-lifecycle-0123456789",
|
||||
"incident_id": "INC-20260715-AG9901",
|
||||
"apply_op_id": "investigating|warning",
|
||||
}
|
||||
}
|
||||
lifecycle_source = {
|
||||
"callback_reply": {
|
||||
**base["callback_reply"],
|
||||
"action": "agent99_lifecycle",
|
||||
}
|
||||
}
|
||||
controlled_source = {
|
||||
"callback_reply": {
|
||||
**base["callback_reply"],
|
||||
"action": "controlled_apply_result",
|
||||
}
|
||||
}
|
||||
|
||||
lifecycle_identity = gateway_service._controlled_apply_result_delivery_identity(
|
||||
lifecycle_source
|
||||
)
|
||||
controlled_identity = gateway_service._controlled_apply_result_delivery_identity(
|
||||
controlled_source
|
||||
)
|
||||
|
||||
assert lifecycle_identity is not None
|
||||
assert controlled_identity is not None
|
||||
assert lifecycle_identity["delivery_kind"] == "agent99_lifecycle"
|
||||
assert controlled_identity["delivery_kind"] == "controlled_apply_result"
|
||||
assert gateway_service._controlled_apply_result_delivery_run_id(
|
||||
lifecycle_identity
|
||||
) == gateway_service._controlled_apply_result_delivery_run_id(lifecycle_identity)
|
||||
assert gateway_service._controlled_apply_result_delivery_run_id(
|
||||
lifecycle_identity
|
||||
) != gateway_service._controlled_apply_result_delivery_run_id(controlled_identity)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifecycle_service_uses_durable_canonical_sender(monkeypatch) -> None:
|
||||
calls: list[dict] = []
|
||||
|
||||
class Gateway:
|
||||
async def send_agent99_lifecycle_receipt(self, **kwargs):
|
||||
calls.append(kwargs)
|
||||
return {
|
||||
"ok": True,
|
||||
"result": {"message_id": 9123},
|
||||
"_awooop_outbound_mirror_acknowledged": True,
|
||||
"_awooop_delivery_status": "sent",
|
||||
"_awooop_provider_send_performed": True,
|
||||
"_awooop_delivery_context": {
|
||||
"destination_binding_verified": True,
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
lifecycle_service,
|
||||
"get_telegram_gateway",
|
||||
lambda: Gateway(),
|
||||
)
|
||||
request = Agent99TelegramLifecycleRequest.model_validate(payload())
|
||||
|
||||
receipt = await lifecycle_service.deliver_agent99_telegram_lifecycle(request)
|
||||
|
||||
assert receipt["ok"] is True
|
||||
assert receipt["durable_outbound_acknowledged"] is True
|
||||
assert receipt["destination_binding_verified"] is True
|
||||
assert receipt["provider_message_id"] == "9123"
|
||||
assert calls[0]["priority"] == "P1"
|
||||
assert "<b>Agent99 事件" in calls[0]["text"]
|
||||
assert "AI 處置" in calls[0]["text"]
|
||||
assert "raw_log" not in calls[0]["text"]
|
||||
Reference in New Issue
Block a user