from __future__ import annotations # ruff: noqa: E402 import base64 import hashlib import os import struct from pathlib import Path from unittest.mock import AsyncMock 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", "trace_id": "trace-agent99-20260715-01", "work_item_id": "work-item-agent99-20260715-01", "executor_name": "Agent99:Recover", "verifier_name": "agent99-recover-post-condition-v1", "received_status": "passed", "check_status": "passed", "apply_status": "running", "verifier_status": "pending", "closure_status": "pending", "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 visual_payload() -> dict: raw = ( b"\x89PNG\r\n\x1a\n" + struct.pack(">I", 13) + b"IHDR" + struct.pack(">II", 1200, 760) + b"\x08\x02\x00\x00\x00" ) return { "schema_version": "agent99_telegram_visual_v1", "media_type": "image/png", "generated_by": "agent99_system_drawing_v1", "width": 1200, "height": 760, "sha256": hashlib.sha256(raw).hexdigest(), "content_base64": base64.b64encode(raw).decode(), } def app_client() -> TestClient: app = FastAPI() app.include_router(agents_api.router, prefix="/api/v1") return TestClient(app) def test_agent99_client_retries_one_202_with_exact_db_only_receipt() -> None: source = ( Path(__file__).resolve().parents[3] / "agent99-control-plane.ps1" ).read_text(encoding="utf-8-sig") send_section = source.split("function Send-AgentTelegram {", 1)[1].split( "function Record-AgentEvent {", 1, )[0] assert send_section.count("Invoke-AgentCanonicalLifecycleIngress ") == 2 assert 'if ([int]$ingress.statusCode -eq 202)' in send_section assert '"agent99_telegram_lifecycle_reconcile_v1"' in send_section assert '$payload["reconcile_receipt"] = $reconcileReceipt' in send_section assert "Start-Sleep -Seconds ([int]$ingress.retryAfterSeconds)" in send_section assert "[int]$ingress.retryAfterSeconds -le 2" in send_section assert "canonicalIngressAttemptCount = 2" in send_section assert "reconcileProviderSendPerformed" in send_section assert "provider_message_id" in send_section assert "destination_binding" in send_section for lifecycle_field in ( "trace_id", "work_item_id", "executor_name", "verifier_name", "received_status", "check_status", "apply_status", "verifier_status", "closure_status", ): assert f"{lifecycle_field} =" in send_section for card_field in ( "receivedStatus", "checkStatus", "applyStatus", "verifierStatus", "closureStatus", "traceKey", "workItemKey", ): assert f"$card.{card_field}" in send_section assert "X-Agent99-Lifecycle-Token" not in send_section selftest_section = source.split( "function Invoke-AgentTelegramDeliverySelfTest {", 1, )[1].split("function Invoke-AgentSensorGateSelfTest {", 1)[0] assert 'id = $testId' in selftest_section assert selftest_section.count( 'Send-AgentTelegram "warning" "telegram_delivery_test"' ) == 1 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"} ) with pytest.raises(ValidationError): Agent99TelegramLifecycleRequest.model_validate( { **payload(), "visual": {**visual_payload(), "sha256": "0" * 64}, } ) with pytest.raises(ValidationError, match="reconcile_identity_mismatch"): Agent99TelegramLifecycleRequest.model_validate( { **payload(), "reconcile_receipt": { "schema_version": ( "agent99_telegram_lifecycle_reconcile_v1" ), "delivery_id": payload()["delivery_id"], "incident_id": "INC-WRONG", "state_key": payload()["state_key"], "provider_message_id": "8123", "destination_binding": "a" * 64, }, } ) with pytest.raises(ValidationError): Agent99TelegramLifecycleRequest.model_validate( {**payload(), "closure_status": "resolved_without_receipt"} ) def test_lifecycle_card_exposes_agent99_stage_and_identity_receipts() -> None: request = Agent99TelegramLifecycleRequest.model_validate( { **payload(), "check_status": "failed", "apply_status": "passed", "verifier_status": "failed", "closure_status": "failed", } ) card = lifecycle_service._format_lifecycle_card(request) caption = lifecycle_service._format_lifecycle_caption(request) assert "AI Agent 執行鏈" in card assert "Received:完成 / PASSED" in card assert "Check:失敗 / FAILED" in card assert "Apply:完成 / PASSED" in card assert "Verifier:失敗 / FAILED" in card assert "Closure:失敗 / FAILED" in card assert "run-agent99-20260715-01" in card assert "trace-agent99-20260715-01" in card assert "work-item-agent99-20260715-01" in card assert "Agent99:Recover" in card assert "agent99-recover-post-condition-v1" in card assert "Received:PASSED" in caption assert "Check:FAILED" in caption assert "Closure:FAILED" in caption assert "Trace:trace-agent99-20260715-01" in caption def test_lifecycle_card_missing_stage_evidence_stays_visibly_pending() -> None: optional_receipt_fields = { "run_id", "trace_id", "work_item_id", "executor_name", "verifier_name", "received_status", "check_status", "apply_status", "verifier_status", "closure_status", } request = Agent99TelegramLifecycleRequest.model_validate( { key: value for key, value in payload().items() if key not in optional_receipt_fields } ) card = lifecycle_service._format_lifecycle_card(request) caption = lifecycle_service._format_lifecycle_caption(request) assert card.count("待回報 / PENDING") >= 5 assert card.count("未提供 / PENDING") >= 5 assert "Received:PENDING" in caption assert "Closure:PENDING" in caption assert "ID|Run:未提供 / PENDING" in caption def test_visual_caption_stays_within_telegram_limit_without_broken_html() -> None: request = Agent99TelegramLifecycleRequest.model_validate( { **payload(), "title": "&" * 120, "target": "&" * 120, "action": "&" * 360, "verification": "&" * 300, "duration_text": "&" * 80, } ) caption = lifecycle_service._format_lifecycle_caption(request) assert len(caption) <= 1024 assert caption.count("") == caption.count("") == 2 assert caption.count("") == caption.count("") == 1 assert "&" not in caption.replace("&", "") 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_returns_structured_pending_receipt(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 == 202 assert response.headers["retry-after"] == "2" assert response.json()["ok"] is False assert response.json()["delivery_status"] == "pending_unknown" assert response.json()["delivery_id"] == payload()["delivery_id"] def test_lifecycle_endpoint_fails_closed_for_non_pending_delivery(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": "durable_readback_failed", } 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 "durable_readback_failed" 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_endpoint_202_retry_is_db_only_and_returns_200( monkeypatch, ) -> None: monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "expected") destination_binding = "a" * 64 calls = {"deliver": 0, "reconcile": 0} async def pending_delivery(request): calls["deliver"] += 1 assert request.reconcile_receipt is None return { "schema_version": "agent99_telegram_lifecycle_receipt_v1", "ok": False, "delivery_id": request.delivery_id, "incident_id": request.incident_id, "delivery_status": "pending_unknown", "provider_message_id": "8124", "destination_binding": destination_binding, "provider_send_performed": True, "reconcile_receipt": { "schema_version": "agent99_telegram_lifecycle_reconcile_v1", "delivery_id": request.delivery_id, "incident_id": request.incident_id, "state_key": request.state_key, "provider_message_id": "8124", "destination_binding": destination_binding, }, } async def durable_reconcile(request): calls["reconcile"] += 1 assert request.reconcile_receipt is not None assert request.reconcile_receipt.provider_message_id == "8124" assert ( request.reconcile_receipt.destination_binding == destination_binding ) return { "schema_version": "agent99_telegram_lifecycle_receipt_v1", "ok": True, "delivery_id": request.delivery_id, "incident_id": request.incident_id, "delivery_status": "sent_reused", "provider_message_id": "8124", "destination_binding": destination_binding, "durable_outbound_acknowledged": True, "destination_binding_verified": True, "provider_send_performed": False, } monkeypatch.setattr( agents_api, "deliver_agent99_telegram_lifecycle", pending_delivery, ) monkeypatch.setattr( agents_api, "reconcile_agent99_telegram_lifecycle", durable_reconcile, ) client = app_client() first = client.post( "/api/v1/agents/agent99/telegram-lifecycle", headers={"X-Agent99-Lifecycle-Token": "expected"}, json=payload(), ) assert first.status_code == 202 assert first.headers["retry-after"] == "2" retry_payload = { **payload(), "reconcile_receipt": first.json()["reconcile_receipt"], } second = client.post( "/api/v1/agents/agent99/telegram-lifecycle", headers={"X-Agent99-Lifecycle-Token": "expected"}, json=retry_payload, ) assert second.status_code == 200 assert second.json()["delivery_status"] == "sent_reused" assert second.json()["provider_send_performed"] is False assert calls == {"deliver": 1, "reconcile": 1} 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) def test_lifecycle_outbox_persists_destination_and_visual_bindings() -> None: destination_binding = "a" * 64 envelope = gateway_service._merge_outbound_source_envelope_extra( {"adapter": "legacy_telegram_gateway"}, { "canonical_route_receipt": { "destination_binding": destination_binding, }, "visual_delivery": { "requested": True, "raw_visual_stored": False, }, }, ) assert envelope["canonical_route_receipt"] == { "destination_binding": destination_binding, } assert envelope["visual_delivery"] == { "requested": True, "raw_visual_stored": False, } @pytest.mark.asyncio async def test_lifecycle_durable_readback_binds_exact_identity_without_egress( monkeypatch, ) -> None: gateway = object.__new__(gateway_service.TelegramGateway) destination_binding = "a" * 64 reconcile = AsyncMock( return_value={ "status": "sent", "provider_message_id": "8124", "destination_binding": destination_binding, "visual_requested": True, } ) monkeypatch.setattr( gateway, "_reconcile_controlled_apply_result_outbound", reconcile, ) result = await gateway.reconcile_agent99_lifecycle_receipt( delivery_id=payload()["delivery_id"], incident_id=payload()["incident_id"], state_key=payload()["state_key"], expected_provider_message_id="8124", expected_destination_binding=destination_binding, ) assert result["ok"] is True assert result["_awooop_delivery_status"] == "sent_reused" assert result["_awooop_provider_send_performed"] is False assert result["_awooop_durable_readback_performed"] is True assert result["_awooop_delivery_context"] == { "destination_binding": destination_binding, "destination_binding_verified": True, "durable_exact_row_readback": True, } reconcile.assert_awaited_once_with( identity={ "project_id": "awoooi", "automation_run_id": payload()["delivery_id"], "incident_id": payload()["incident_id"], "apply_op_id": payload()["state_key"], "delivery_kind": "agent99_lifecycle", }, expected_provider_message_id="8124", expected_destination_binding=destination_binding, ) @pytest.mark.asyncio async def test_lifecycle_reconcile_destination_mismatch_never_finalizes( monkeypatch, ) -> None: gateway = object.__new__(gateway_service.TelegramGateway) readback = AsyncMock( return_value={ "status": "pending_unknown", "run_id": "00000000-0000-0000-0000-000000000201", "message_id": "00000000-0000-0000-0000-000000000202", "destination_binding": "a" * 64, } ) finalize = AsyncMock(return_value=True) monkeypatch.setattr( gateway, "_read_controlled_apply_result_outbound", readback, ) monkeypatch.setattr( gateway, "_finalize_controlled_apply_result_outbound", finalize, ) result = await gateway._reconcile_controlled_apply_result_outbound( identity={ "project_id": "awoooi", "automation_run_id": payload()["delivery_id"], "incident_id": payload()["incident_id"], "apply_op_id": payload()["state_key"], "delivery_kind": "agent99_lifecycle", }, expected_provider_message_id="8124", expected_destination_binding="b" * 64, ) assert result["status"] == "destination_binding_mismatch" finalize.assert_not_awaited() @pytest.mark.asyncio async def test_lifecycle_reconcile_service_never_enters_provider_sender( monkeypatch, ) -> None: destination_binding = "a" * 64 calls: list[dict] = [] class Gateway: async def send_agent99_lifecycle_receipt(self, **_kwargs): raise AssertionError("provider sender must not run during reconcile") async def reconcile_agent99_lifecycle_receipt(self, **kwargs): calls.append(kwargs) return { "ok": True, "result": {"message_id": "8124"}, "_awooop_outbound_mirror_acknowledged": True, "_awooop_delivery_status": "sent_reused", "_awooop_provider_send_performed": False, "_awooop_delivery_context": { "destination_binding": destination_binding, "destination_binding_verified": True, "durable_exact_row_readback": True, }, } monkeypatch.setattr( lifecycle_service, "get_telegram_gateway", lambda: Gateway(), ) request = Agent99TelegramLifecycleRequest.model_validate( { **payload(), "reconcile_receipt": { "schema_version": "agent99_telegram_lifecycle_reconcile_v1", "delivery_id": payload()["delivery_id"], "incident_id": payload()["incident_id"], "state_key": payload()["state_key"], "provider_message_id": "8124", "destination_binding": destination_binding, }, } ) receipt = await lifecycle_service.reconcile_agent99_telegram_lifecycle( request ) assert receipt["ok"] is True assert receipt["delivery_status"] == "sent_reused" assert receipt["provider_send_performed"] is False assert receipt["destination_binding_verified"] is True assert calls == [ { "delivery_id": payload()["delivery_id"], "incident_id": payload()["incident_id"], "state_key": payload()["state_key"], "project_id": "awoooi", "expected_provider_message_id": "8124", "expected_destination_binding": destination_binding, } ] @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"] @pytest.mark.asyncio async def test_lifecycle_reconciles_pending_send_without_duplicate_provider_message( monkeypatch, ) -> None: send_calls: list[dict] = [] reconcile_calls: list[dict] = [] sleeps: list[float] = [] initial_response = { "ok": True, "result": {"message_id": 9126}, "_awooop_outbound_mirror_acknowledged": False, "_awooop_delivery_status": "pending_unknown", "_awooop_provider_send_performed": True, "_awooop_visual_delivery_verified": True, "_awooop_delivery_context": { "destination_binding_verified": True, "destination_binding": "a" * 64, }, } reconciled_response = { "ok": True, "result": {"message_id": 9126}, "_awooop_outbound_mirror_acknowledged": True, "_awooop_delivery_status": "sent_reused", "_awooop_provider_send_performed": False, "_awooop_visual_delivery_verified": True, } class Gateway: async def send_agent99_lifecycle_receipt(self, **kwargs): send_calls.append(kwargs) return initial_response async def reconcile_agent99_lifecycle_receipt(self, **kwargs): reconcile_calls.append(kwargs) return reconciled_response async def no_sleep(delay: float) -> None: sleeps.append(delay) monkeypatch.setattr( lifecycle_service, "get_telegram_gateway", lambda: Gateway(), ) monkeypatch.setattr(lifecycle_service.asyncio, "sleep", no_sleep) request = Agent99TelegramLifecycleRequest.model_validate( {**payload(), "visual": visual_payload()} ) receipt = await lifecycle_service.deliver_agent99_telegram_lifecycle(request) assert receipt["ok"] is True assert receipt["delivery_status"] == "sent_reused" assert receipt["durable_outbound_acknowledged"] is True assert receipt["provider_message_id"] == "9126" assert receipt["provider_send_performed"] is True assert receipt["durable_reconciliation_attempts"] == 1 assert receipt["visual_sent"] is True assert sleeps == [0.25] assert len(send_calls) == 1 assert len(reconcile_calls) == 1 assert send_calls[0]["delivery_id"] == payload()["delivery_id"] assert reconcile_calls[0] == { "delivery_id": payload()["delivery_id"], "incident_id": payload()["incident_id"], "state_key": payload()["state_key"], "project_id": "awoooi", "expected_provider_message_id": "9126", "expected_destination_binding": "a" * 64, } @pytest.mark.asyncio async def test_lifecycle_follower_only_reads_inflight_durable_delivery( monkeypatch, ) -> None: send_calls: list[dict] = [] reconcile_calls: list[dict] = [] class Gateway: async def send_agent99_lifecycle_receipt(self, **kwargs): send_calls.append(kwargs) return { "ok": False, "_awooop_outbound_mirror_acknowledged": False, "_awooop_delivery_status": "pending_unknown", "_awooop_provider_send_performed": False, } async def reconcile_agent99_lifecycle_receipt(self, **kwargs): reconcile_calls.append(kwargs) return { "ok": True, "result": {"message_id": 9128}, "_awooop_outbound_mirror_acknowledged": True, "_awooop_delivery_status": "sent_reused", "_awooop_provider_send_performed": False, "_awooop_delivery_context": { "destination_binding_verified": True, "durable_exact_row_readback": True, }, } async def no_sleep(_delay: float) -> None: return None monkeypatch.setattr( lifecycle_service, "get_telegram_gateway", lambda: Gateway(), ) monkeypatch.setattr(lifecycle_service.asyncio, "sleep", no_sleep) receipt = await lifecycle_service.deliver_agent99_telegram_lifecycle( Agent99TelegramLifecycleRequest.model_validate(payload()) ) assert receipt["ok"] is True assert receipt["provider_message_id"] == "9128" assert receipt["provider_send_performed"] is False assert len(send_calls) == 1 assert len(reconcile_calls) == 1 assert reconcile_calls[0]["expected_provider_message_id"] is None assert reconcile_calls[0]["expected_destination_binding"] is None @pytest.mark.asyncio async def test_lifecycle_reconciliation_fails_closed_after_bounded_readbacks( monkeypatch, ) -> None: send_calls: list[dict] = [] reconcile_calls: list[dict] = [] sleeps: list[float] = [] class Gateway: async def send_agent99_lifecycle_receipt(self, **kwargs): send_calls.append(kwargs) return { "ok": True, "result": {"message_id": 9127}, "_awooop_outbound_mirror_acknowledged": False, "_awooop_delivery_status": "pending_unknown", "_awooop_provider_send_performed": True, "_awooop_delivery_context": { "destination_binding_verified": True, "destination_binding": "a" * 64, }, } async def reconcile_agent99_lifecycle_receipt(self, **kwargs): reconcile_calls.append(kwargs) return { "ok": False, "_awooop_outbound_mirror_acknowledged": False, "_awooop_delivery_status": "pending_unknown", "_awooop_provider_send_performed": False, "_awooop_delivery_context": { "destination_binding_verified": False, "durable_exact_row_readback": True, }, } async def no_sleep(delay: float) -> None: sleeps.append(delay) monkeypatch.setattr( lifecycle_service, "get_telegram_gateway", lambda: Gateway(), ) monkeypatch.setattr(lifecycle_service.asyncio, "sleep", no_sleep) request = Agent99TelegramLifecycleRequest.model_validate(payload()) receipt = await lifecycle_service.deliver_agent99_telegram_lifecycle(request) assert receipt["ok"] is False assert receipt["delivery_status"] == "pending_unknown" assert receipt["durable_outbound_acknowledged"] is False assert receipt["provider_send_performed"] is True assert receipt["durable_reconciliation_attempts"] == 3 assert sleeps == [0.25, 0.5, 1.0] assert len(send_calls) == 1 assert len(reconcile_calls) == 3 assert all( call["expected_provider_message_id"] == "9127" for call in reconcile_calls ) assert all( call["expected_destination_binding"] == "a" * 64 for call in reconcile_calls ) @pytest.mark.asyncio async def test_visual_lifecycle_uses_one_durable_send_photo_receipt(monkeypatch) -> None: calls: list[dict] = [] class Gateway: async def send_agent99_lifecycle_receipt(self, **kwargs): calls.append(kwargs) return { "ok": True, "result": {"message_id": 9124}, "_awooop_outbound_mirror_acknowledged": True, "_awooop_delivery_status": "sent", "_awooop_provider_send_performed": True, "_awooop_visual_delivery_verified": True, "_awooop_delivery_context": { "destination_binding_verified": True, }, } monkeypatch.setattr( lifecycle_service, "get_telegram_gateway", lambda: Gateway(), ) request = Agent99TelegramLifecycleRequest.model_validate( {**payload(), "visual": visual_payload()} ) receipt = await lifecycle_service.deliver_agent99_telegram_lifecycle(request) assert receipt["ok"] is True assert receipt["visual_requested"] is True assert receipt["visual_sent"] is True assert receipt["raw_visual_stored"] is False assert calls[0]["visual_png"].startswith(b"\x89PNG") assert calls[0]["visual_sha256"] == visual_payload()["sha256"] assert len(calls[0]["text"]) <= 1024 assert "Agent99:" in calls[0]["text"] @pytest.mark.asyncio async def test_reused_legacy_text_receipt_does_not_claim_visual_delivery( monkeypatch, ) -> None: class Gateway: async def send_agent99_lifecycle_receipt(self, **kwargs): return { "ok": True, "result": {"message_id": 7001}, "_awooop_outbound_mirror_acknowledged": True, "_awooop_delivery_status": "sent_reused", "_awooop_provider_send_performed": False, } monkeypatch.setattr( lifecycle_service, "get_telegram_gateway", lambda: Gateway(), ) request = Agent99TelegramLifecycleRequest.model_validate( {**payload(), "visual": visual_payload()} ) receipt = await lifecycle_service.deliver_agent99_telegram_lifecycle(request) assert receipt["ok"] is True assert receipt["visual_requested"] is True assert receipt["visual_sent"] is False assert receipt["visual_delivery_status"] == "sent_reused" @pytest.mark.asyncio async def test_gateway_builds_primary_photo_with_same_send_once_identity() -> None: calls: list[tuple[str, dict]] = [] gateway = object.__new__(gateway_service.TelegramGateway) async def capture(method: str, outgoing: dict) -> dict: calls.append((method, outgoing)) return {"ok": True} gateway._send_request = capture image = Agent99TelegramLifecycleRequest.model_validate( {**payload(), "visual": visual_payload()} ).visual assert image is not None await gateway.send_agent99_lifecycle_receipt( delivery_id=payload()["delivery_id"], incident_id=payload()["incident_id"], state_key=payload()["state_key"], text="short caption", priority="P1", visual_png=image.png_bytes(), visual_sha256=image.sha256, ) method, outgoing = calls[0] assert method == "sendPhoto" assert outgoing["photo"] == "attach://photo" assert outgoing["caption"] == "short caption" assert outgoing[gateway_service._BINARY_UPLOAD_KEY]["content"].startswith( b"\x89PNG" ) source = outgoing[gateway_service._AWOOOP_SOURCE_ENVELOPE_EXTRA_KEY] assert source["callback_reply"]["automation_run_id"] == payload()["delivery_id"] assert source["visual_delivery"]["raw_visual_stored"] is False @pytest.mark.asyncio async def test_gateway_posts_visual_as_multipart_without_binary_json(monkeypatch) -> None: requests: list[tuple[str, dict]] = [] class Response: def raise_for_status(self) -> None: return None def json(self) -> dict: return { "ok": True, "result": {"message_id": 9125, "chat": {"id": "-10099"}}, } class Client: async def post(self, url: str, **kwargs): requests.append((url, kwargs)) return Response() monkeypatch.setattr(settings, "OPENCLAW_TG_BOT_TOKEN", "test-token") gateway = object.__new__(gateway_service.TelegramGateway) gateway._initialized = True gateway._http_client = Client() binding = gateway_service._telegram_destination_binding("-10099") gateway._authorize_canonical_send_message = lambda **_: ( "-10099", { "schema_version": "telegram_canonical_egress_receipt_v1", "decision": "allow", "classifier": "canonical_route", "product_id": "awoooi", "signal_family": "incident_lifecycle", "severity": "P1", "destination_alias": "telegram_chat_alias:awoooi_sre_war_room", "destination_binding": binding, "sender_bot_alias": "tsenyang_bot", "provider_send_performed": False, }, ) image = Agent99TelegramLifecycleRequest.model_validate( {**payload(), "visual": visual_payload()} ).visual assert image is not None receipt = await gateway._send_request( "sendPhoto", { "photo": "attach://photo", "caption": "Agent99 visual test", gateway_service._CANONICAL_ROUTE_CONTEXT_KEY: { "product_id": "awoooi", "signal_family": "incident_lifecycle", "severity": "P1", }, gateway_service._BINARY_UPLOAD_KEY: { "field_name": "photo", "filename": "agent99-card.png", "content_type": "image/png", "content": image.png_bytes(), }, }, ) assert receipt["_awooop_delivery_status"] == "sent" _, request_kwargs = requests[0] assert "json" not in request_kwargs assert request_kwargs["data"]["photo"] == "attach://photo" assert request_kwargs["files"]["photo"][0] == "agent99-card.png" assert request_kwargs["files"]["photo"][1].startswith(b"\x89PNG")