220 lines
6.7 KiB
Python
220 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from src.api.v1.ai import get_ai_status
|
|
from src.api.v1.health import get_ai_usage
|
|
from src.core import config as config_module
|
|
from src.services import ai_control as ai_control_module
|
|
from src.services import ai_rate_limiter as limiter_module
|
|
from src.services.ai_provider_policy import production_route_readback
|
|
|
|
PRODUCTION_PROVIDER_ORDER = [
|
|
"ollama_gcp_a",
|
|
"ollama_gcp_b",
|
|
"ollama_local",
|
|
"gemini",
|
|
]
|
|
|
|
|
|
def _usage(*, authenticated: bool | None = None) -> dict:
|
|
return {
|
|
"provider": "gemini",
|
|
"limited": True,
|
|
"readback_status": "verified",
|
|
"cost_exceeded": False,
|
|
"accounting": {
|
|
"complete": True,
|
|
"status": "receipt_backed",
|
|
},
|
|
"pricing_policy": {
|
|
"model": "gemini-2.5-flash-lite",
|
|
"supported": True,
|
|
},
|
|
"total_cost_usd": {
|
|
"current": 0.0,
|
|
"reserved": 0.0,
|
|
"limit": 5.0,
|
|
"alert_threshold": 4.0,
|
|
},
|
|
"authentication": {
|
|
"provider": "gemini",
|
|
"configured": True,
|
|
"authenticated": authenticated,
|
|
"authentication_verified": authenticated is True,
|
|
"verification_status": (
|
|
"authenticated_verified_by_generation"
|
|
if authenticated is True
|
|
else "configured_not_verified"
|
|
),
|
|
"verification_source": (
|
|
"durable_generation_receipt" if authenticated is True else None
|
|
),
|
|
"verified_at": "2026-07-14T12:00:00+08:00" if authenticated else None,
|
|
},
|
|
}
|
|
|
|
|
|
class _Limiter:
|
|
def __init__(self, usage: dict | None = None, error: Exception | None = None) -> None:
|
|
self.usage = usage
|
|
self.error = error
|
|
|
|
async def get_usage_stats(self, provider: str) -> dict:
|
|
assert provider == "gemini"
|
|
if self.error:
|
|
raise self.error
|
|
assert self.usage is not None
|
|
return dict(self.usage)
|
|
|
|
|
|
def _configure(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
*,
|
|
limiter: _Limiter,
|
|
disabled: bool = False,
|
|
) -> None:
|
|
monkeypatch.setattr(
|
|
config_module.settings,
|
|
"GEMINI_API_KEY",
|
|
"configured-test-value",
|
|
)
|
|
monkeypatch.setenv("ENABLE_GEMINI", "true")
|
|
monkeypatch.setattr(limiter_module, "get_ai_rate_limiter", lambda: limiter)
|
|
monkeypatch.setattr(
|
|
ai_control_module,
|
|
"is_provider_disabled",
|
|
AsyncMock(return_value=disabled),
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ai_status_separates_configured_authenticated_and_cost_guard(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_configure(monkeypatch, limiter=_Limiter(_usage(authenticated=None)))
|
|
|
|
result = await get_ai_status()
|
|
|
|
route = result["production_route"]
|
|
assert result["fallback_order"] == PRODUCTION_PROVIDER_ORDER
|
|
assert [hop["identity"] for hop in route["hops"]] == [
|
|
"GCP-A",
|
|
"GCP-B",
|
|
"host111",
|
|
"Gemini API",
|
|
]
|
|
assert result["gemini_configured"] is True
|
|
assert result["gemini_enabled"] is True
|
|
assert result["gemini_authenticated"] is None
|
|
assert result["gemini_authentication_verified"] is False
|
|
assert result["gemini_production_executable"] is False
|
|
assert "configured-test-value" not in repr(result)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disabled_gemini_remains_visible_but_non_executable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_configure(
|
|
monkeypatch,
|
|
limiter=_Limiter(_usage(authenticated=True)),
|
|
disabled=True,
|
|
)
|
|
|
|
result = await get_ai_status()
|
|
|
|
assert result["fallback_order"] == PRODUCTION_PROVIDER_ORDER
|
|
assert result["gemini_enabled"] is False
|
|
assert result["gemini_production_executable"] is False
|
|
assert result["production_route"]["hops"][3]["production_executable"] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verified_auth_and_complete_accounting_enable_cost_guard_readback(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_configure(monkeypatch, limiter=_Limiter(_usage(authenticated=True)))
|
|
|
|
result = await get_ai_status()
|
|
|
|
assert result["production_route"]["gemini"]["cost_guard_ready"] is True
|
|
assert result["gemini_production_executable"] is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"accounting,readback_status",
|
|
[
|
|
({}, "verified"),
|
|
({"complete": True}, "verified"),
|
|
({"complete": False, "status": "receipt_backed"}, "verified"),
|
|
({"complete": True, "status": "degraded_missing_receipt"}, "verified"),
|
|
(
|
|
{"complete": True, "status": "receipt_backed", "degraded": True},
|
|
"verified",
|
|
),
|
|
({"complete": True, "status": "receipt_backed"}, "degraded"),
|
|
],
|
|
)
|
|
def test_cost_guard_static_readback_fails_closed_without_complete_nondegraded_accounting(
|
|
accounting: dict,
|
|
readback_status: str,
|
|
) -> None:
|
|
cost_guard = _usage(authenticated=True)
|
|
cost_guard["accounting"] = accounting
|
|
cost_guard["readback_status"] = readback_status
|
|
|
|
route = production_route_readback(
|
|
gemini_configured=True,
|
|
gemini_enabled=True,
|
|
gemini_authentication=cost_guard["authentication"],
|
|
cost_guard=cost_guard,
|
|
)
|
|
|
|
assert route["gemini"]["cost_guard_ready"] is False
|
|
assert route["gemini"]["production_executable"] is False
|
|
assert route["hops"][3]["production_executable"] is False
|
|
|
|
|
|
@pytest.mark.parametrize("missing_field", ["pricing_policy", "cost_exceeded"])
|
|
def test_cost_guard_static_readback_requires_affirmative_pricing_and_cost_proof(
|
|
missing_field: str,
|
|
) -> None:
|
|
cost_guard = _usage(authenticated=True)
|
|
cost_guard.pop(missing_field)
|
|
|
|
route = production_route_readback(
|
|
gemini_configured=True,
|
|
gemini_enabled=True,
|
|
gemini_authentication=cost_guard["authentication"],
|
|
cost_guard=cost_guard,
|
|
)
|
|
|
|
assert route["gemini"]["cost_guard_ready"] is False
|
|
assert route["gemini"]["production_executable"] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_status_endpoints_fail_closed_when_cost_readback_is_unavailable(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
_configure(
|
|
monkeypatch,
|
|
limiter=_Limiter(error=RuntimeError("isolated readback failure")),
|
|
)
|
|
|
|
status = await get_ai_status()
|
|
health = await get_ai_usage()
|
|
|
|
assert status["gemini_production_executable"] is False
|
|
assert status["production_route"]["gemini"]["cost_guard_ready"] is False
|
|
assert health["gemini"]["readback_status"] == "unavailable"
|
|
assert health["gemini_status"]["production_executable"] is False
|
|
assert health["claude"] == {
|
|
"provider": "claude",
|
|
"production_executable": False,
|
|
"mode": "non_executable_shadow_metadata_only",
|
|
}
|