from __future__ import annotations
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.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)
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 send_as_nemotron.await_args.kwargs == {
"text": outbound,
"reply_to_message_id": 456,
"inbound_chat_id": -100123,
}
@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