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

209 lines
7.2 KiB
Python

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"]