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

446 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 "Claudeconfigured=trueauthenticated=true" in outbound
assert "Geminiconfigured=trueauthenticated=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: <b>$0.011802</b> | receipt=verified" in outbound
assert "Gemini: <b>$0.000297</b> | receipt=verified" in outbound
assert "合計: <b>$0.012099</b>" in outbound
assert "receipt-backed" in outbound
assert "Flash-Lite" not in outbound
assert "Haiku" not in outbound