209 lines
7.2 KiB
Python
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"]
|