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 "Agent99 事件" in calls[0]["text"] assert "AI 處置" in calls[0]["text"] assert "raw_log" not in calls[0]["text"]