from __future__ import annotations from types import SimpleNamespace from unittest.mock import AsyncMock import pytest from src.services import ai_rate_limiter as limiter_module from src.services import chat_manager as chat_module from src.services import telegram_gateway as gateway_module from src.services.chat_manager import ChatManager from src.services.telegram_gateway import ( TelegramGateway, _sre_group_message_requires_ai_response, ) class _ReceiptBackedCostLimiter: async def get_usage_stats(self, provider: str) -> dict: return { "provider": provider, "readback_status": "verified", "total_cost_usd": { "current": 0.011802 if provider == "claude" else 0.000297, }, "accounting": { "complete": True, "status": "receipt_backed", "degraded": False, }, } @pytest.mark.parametrize( "message", [ r"new-item -itemtype directory -force -path \wooo\agent99\tmp", r"set-content -encoding ascii -path \wooo\agent99\tmp\fingerprint.txt", r"add-content -encoding ascii -path \wooo\agent99\tmp\events.log", "kubectl get pods -n awoooi-prod", "2026-07-19T00:26:31+08:00 ERROR provider timeout", '{"event":"agent99_runtime_receipt","status":"pending"}', "```powershell\nRestart-Service alertmanager\n```", ], ) def test_unaddressed_machine_payload_does_not_request_ai(message: str) -> None: assert _sre_group_message_requires_ai_response(message) is False @pytest.mark.parametrize( "message,reply_to_message", [ ("kubectl get pods 為什麼失敗?", None), ("@OpenClawAwoooI_Bot kubectl get pods 失敗", None), ("set-content 這行是什麼意思", None), ("目前有哪些異常服務", None), ( "Restart-Service alertmanager", {"from": {"is_bot": True, "username": "OpenClawAwoooI_Bot"}}, ), ], ) def test_explicit_dialogue_about_machine_payload_still_requests_ai( message: str, reply_to_message: dict | None, ) -> None: assert ( _sre_group_message_requires_ai_response(message, reply_to_message) is True ) @pytest.mark.parametrize( "message,expected", [ ("Gemini provider 現在可用嗎?", "provider_status"), ("請重啟 alertmanager", "controlled_action_request"), ("這個告警的根因是什麼?", "knowledge_query"), ("目前有哪些異常服務", "incident_status"), ("幫我整理今天的 SRE 重點", "general_sre_question"), ], ) def test_group_intent_is_classified_without_model_call( message: str, expected: str, ) -> None: assert ChatManager.classify_intent(message) == expected @pytest.mark.asyncio async def test_group_rag_context_is_bounded_and_private_ip_redacted( monkeypatch: pytest.MonkeyPatch, ) -> None: class _KnowledgeService: semantic_search = AsyncMock( return_value=[ ( SimpleNamespace( id="km-alertmanager-001", title="Alertmanager runbook", content="檢查 192.168.0.110 的 webhook 狀態。" * 80, ), 0.91, ) ] ) from src.services import knowledge_service as knowledge_module knowledge_service = _KnowledgeService() monkeypatch.setattr( knowledge_module, "get_knowledge_service", lambda: knowledge_service, ) receipt = await ChatManager().get_rag_context( "Alertmanager 192.168.0.110 為什麼異常?" ) assert receipt["status"] == "ready" assert receipt["source_refs"] == ["km-alertmanager-001"] assert len(receipt["prompt_context"]) <= 1800 assert "192.168.0.110" not in receipt["prompt_context"] assert "[PRIVATE_IP_REDACTED]" in receipt["prompt_context"] query = knowledge_service.semantic_search.await_args.args[0] assert "192.168.0.110" not in query assert "[PRIVATE_IP_REDACTED]" in query @pytest.mark.asyncio async def test_group_route_rejects_unverified_inbound_receipt_before_provider( monkeypatch: pytest.MonkeyPatch, ) -> None: def _provider_call_forbidden(): raise AssertionError("unverified ingress must not call provider") monkeypatch.setattr(chat_module, "get_ai_executor", _provider_call_forbidden) result = await ChatManager().route_chat( role="openclaw", system_prompt="bounded", user_message="目前有哪些異常?", route_context={ "source_channel": "telegram_sre_war_room", "canonical_chat_identity": { "status": "verified", "room": "awoooi_sre_war_room", }, "intent": "incident_status", }, ) assert result.success is False assert result.fallback_reason == "telegram_identity_receipt_unverified" assert result.provider == "none" @pytest.mark.asyncio async def test_group_machine_payload_stops_before_context_or_provider( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(gateway_module.settings, "HERMES_NL_ENABLED", False) def _chat_manager_forbidden(): raise AssertionError("machine payload must not build context or call AI") monkeypatch.setattr(chat_module, "get_chat_manager", _chat_manager_forbidden) gateway = object.__new__(TelegramGateway) await gateway._handle_group_message( r"new-item -itemtype directory -force -path \wooo\agent99\tmp", user_id=99, username="operator", chat_id=-100123, message_id=455, ) @pytest.mark.asyncio async def test_nemotron_group_mention_uses_structured_provider_status_router( monkeypatch: pytest.MonkeyPatch, ) -> None: """Telegram mention must not invent a Gemini lock or call a provider.""" readback = { "schema_version": "sre_chat_provider_runtime_readback_v1", "policy_order": [ "ollama_gcp_a", "ollama_gcp_b", "ollama_local", "claude", "gemini", ], "ollama_health": { "ollama_gcp_a": "offline", "ollama_gcp_b": "healthy", "ollama_local": "not_checked", }, "paid": { "claude": { "configured": True, "authenticated": True, "authentication_verified": True, "verification_status": "authenticated_verified_by_generation", "execution_mode": "canary", "canary_percent": 5, "route_lease_state": "inactive_disabled_after_canary", "runtime_state": ( "configured_authenticated_canary_waiting_for_run_owned_lease" ), }, "gemini": { "configured": True, "authenticated": False, "authentication_verified": False, "verification_status": "authentication_rejected", "execution_mode": "canary", "canary_percent": 5, "route_lease_state": "inactive_disabled_after_canary", "runtime_state": "configured_canary_authentication_rejected", }, }, "provider_route_switch_performed": False, "infrastructure_remediation_write_allowed": False, "cross_domain_execution_allowed": False, } manager = ChatManager() monkeypatch.setattr( manager, "get_system_context", AsyncMock(return_value="bounded system context"), ) monkeypatch.setattr( manager, "get_provider_runtime_readback", AsyncMock(return_value=readback), ) monkeypatch.setattr(chat_module, "get_chat_manager", lambda: manager) def _provider_call_forbidden(): raise AssertionError("provider executor must not run for a status readback") monkeypatch.setattr(chat_module, "get_ai_executor", _provider_call_forbidden) monkeypatch.setattr(gateway_module.settings, "HERMES_NL_ENABLED", False) gateway = object.__new__(TelegramGateway) send_as_nemotron = AsyncMock(return_value={"ok": True}) monkeypatch.setattr(gateway, "send_as_nemotron", send_as_nemotron) monkeypatch.setattr( gateway, "mirror_sre_group_message_received", AsyncMock( return_value={ "event_id": "event-456", "trace_id": "telegram-message:event-456", "run_id": "run-456", "work_item_id": "AIA-CONV-029", } ), ) monkeypatch.setattr( manager, "get_rag_context", AsyncMock( return_value={ "status": "skipped_deterministic_readback", "source_refs": [], "prompt_context": "", } ), ) await gateway._handle_group_message( "@NemoTronAwoooI_Bot Gemini API 現在是硬鎖定狀態嗎?", user_id=99, username="operator", chat_id=-100123, message_id=456, ) send_as_nemotron.assert_awaited_once() outbound = send_as_nemotron.await_args.kwargs["text"] assert "硬鎖" not in outbound assert "locked" not in outbound.casefold() assert "無法正常回應" not in outbound assert "Claude:configured=true|authenticated=true" in outbound assert "Gemini:configured=true|authenticated=false" in outbound assert "verification=authentication_rejected" in outbound assert "canary=canary 5%" in outbound assert "route_lease=inactive_disabled_after_canary" in outbound assert "provider=runtime_readback" in outbound assert "model=deterministic_policy" in outbound assert "fallback=provider_status_request_no_generation" in outbound assert "cost_receipt=none" in outbound assert "trace=telegram-message:event-456" in outbound assert "intent=provider_status" in outbound assert "rag=skipped_deterministic_readback" in outbound assert "identity=verified" in outbound assert "policy=advisory_no_runtime_mutation" in outbound assert send_as_nemotron.await_args.kwargs == { "text": outbound, "reply_to_message_id": 456, "inbound_chat_id": -100123, } @pytest.mark.asyncio async def test_group_message_fails_closed_before_rag_or_provider_when_mirror_fails( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(gateway_module.settings, "HERMES_NL_ENABLED", False) manager = ChatManager() monkeypatch.setattr(chat_module, "get_chat_manager", lambda: manager) monkeypatch.setattr( manager, "get_system_context", AsyncMock(side_effect=AssertionError("context must not be read")), ) monkeypatch.setattr( manager, "get_rag_context", AsyncMock(side_effect=AssertionError("RAG must not be read")), ) gateway = object.__new__(TelegramGateway) monkeypatch.setattr( gateway, "mirror_sre_group_message_received", AsyncMock(return_value=None), ) send_as_openclaw = AsyncMock(return_value={"ok": True}) monkeypatch.setattr(gateway, "send_as_openclaw", send_as_openclaw) await gateway._handle_group_message( "@OpenClawAwoooI_Bot 目前告警狀態?", user_id=99, username="operator", chat_id=-100123, message_id=457, update_id=9001, ) send_as_openclaw.assert_awaited_once() assert "對話未送入 AI" in send_as_openclaw.await_args.kwargs["text"] manager.get_rag_context.assert_not_awaited() manager.get_system_context.assert_not_awaited() @pytest.mark.asyncio async def test_group_message_passes_one_rag_and_trace_context_to_provider( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(gateway_module.settings, "HERMES_NL_ENABLED", False) manager = ChatManager() monkeypatch.setattr(chat_module, "get_chat_manager", lambda: manager) monkeypatch.setattr( manager, "get_system_context", AsyncMock(return_value="bounded system context"), ) monkeypatch.setattr( manager, "get_rag_context", AsyncMock( return_value={ "status": "ready", "source_refs": ["km-incident-001"], "prompt_context": "[km-incident-001] bounded evidence", } ), ) call_openclaw = AsyncMock(return_value="AI reply") call_nemotron = AsyncMock( side_effect=AssertionError("unmentioned message must use one provider route") ) monkeypatch.setattr(manager, "_call_openclaw", call_openclaw) monkeypatch.setattr(manager, "_call_nemotron", call_nemotron) gateway = object.__new__(TelegramGateway) mirror = AsyncMock( return_value={ "event_id": "event-458", "trace_id": "telegram-message:event-458", "run_id": "run-458", "work_item_id": "AIA-CONV-029", } ) monkeypatch.setattr(gateway, "mirror_sre_group_message_received", mirror) send_as_openclaw = AsyncMock(return_value={"ok": True}) monkeypatch.setattr(gateway, "send_as_openclaw", send_as_openclaw) await gateway._handle_group_message( "這個告警的根因是什麼?", user_id=99, username="operator", chat_id=-100123, message_id=458, update_id=9002, ) manager.get_rag_context.assert_awaited_once() call_openclaw.assert_awaited_once() system_prompt, question = call_openclaw.await_args.args route_context = call_openclaw.await_args.kwargs["route_context"] assert question == "這個告警的根因是什麼?" assert "KM/RAG 參考證據" in system_prompt assert route_context["trace_id"] == "telegram-message:event-458" assert route_context["intent"] == "knowledge_query" assert route_context["rag_retrieval_receipt"] == { "status": "ready", "source_refs": ["km-incident-001"], } assert route_context["canonical_chat_identity"]["status"] == "verified" assert route_context["executor_invocation_allowed"] is False call_nemotron.assert_not_awaited() @pytest.mark.asyncio async def test_group_cost_uses_receipt_backed_accounting_not_legacy_keys( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr( limiter_module, "get_ai_rate_limiter", lambda: _ReceiptBackedCostLimiter(), ) gateway = object.__new__(TelegramGateway) send_as_openclaw = AsyncMock(return_value={"ok": True}) monkeypatch.setattr(gateway, "send_as_openclaw", send_as_openclaw) await gateway._handle_group_command( "/cost", -100123, 789, full_text="/cost", ) send_as_openclaw.assert_awaited_once() outbound = send_as_openclaw.await_args.kwargs["text"] assert "Claude: $0.011802 | receipt=verified" in outbound assert "Gemini: $0.000297 | receipt=verified" in outbound assert "合計: $0.012099" in outbound assert "receipt-backed" in outbound assert "Flash-Lite" not in outbound assert "Haiku" not in outbound