Spaces:
Running
Running
github-actions[bot] commited on
Commit ·
f9f5d3e
1
Parent(s): 9dfc563
deploy: backend from 0d25f4f
Browse files- main.py +25 -1
- startup_validation.py +97 -0
- tests/test_startup_auth_visibility.py +170 -0
main.py
CHANGED
|
@@ -60,7 +60,7 @@ except Exception:
|
|
| 60 |
|
| 61 |
# STARTUP VALIDATION - Run before anything else to prevent restart loops
|
| 62 |
try:
|
| 63 |
-
from startup_validation import run_all_validations
|
| 64 |
run_all_validations() # Exits with error if any critical check fails
|
| 65 |
except ImportError as e:
|
| 66 |
# If startup_validation module is not found, log warning but continue
|
|
@@ -565,8 +565,17 @@ async def app_lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
|
| 565 |
except Exception as exc:
|
| 566 |
logger.error("RAG vectorstore warm-up failed: %s", exc)
|
| 567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
_warmup_inference_task = asyncio.create_task(_warmup_inference_client())
|
| 569 |
_warmup_vectorstore_task = asyncio.create_task(_warmup_vectorstore())
|
|
|
|
| 570 |
|
| 571 |
# FIX(502): Set a readiness flag so /health reports the true state without
|
| 572 |
# triggering heavy init on every health-check ping from HF Spaces' proxy.
|
|
@@ -2325,12 +2334,27 @@ class TestingResetResponse(BaseModel):
|
|
| 2325 |
# to decide whether to route traffic; a slow response or crash here causes 502.
|
| 2326 |
@app.get("/health")
|
| 2327 |
async def health_check():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2328 |
return {
|
| 2329 |
"status": "ready" if _backend_ready else "starting",
|
| 2330 |
"space": "mathpulse-ai",
|
| 2331 |
"firebase": _firebase_ready,
|
| 2332 |
"chat_model": CHAT_MODEL,
|
| 2333 |
"risk_model": RISK_MODEL,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2334 |
}
|
| 2335 |
|
| 2336 |
|
|
|
|
| 60 |
|
| 61 |
# STARTUP VALIDATION - Run before anything else to prevent restart loops
|
| 62 |
try:
|
| 63 |
+
from startup_validation import run_all_validations, validate_deepseek_auth
|
| 64 |
run_all_validations() # Exits with error if any critical check fails
|
| 65 |
except ImportError as e:
|
| 66 |
# If startup_validation module is not found, log warning but continue
|
|
|
|
| 565 |
except Exception as exc:
|
| 566 |
logger.error("RAG vectorstore warm-up failed: %s", exc)
|
| 567 |
|
| 568 |
+
async def _warmup_deepseek_auth() -> None:
|
| 569 |
+
auth_state = await asyncio.to_thread(validate_deepseek_auth, 5.0)
|
| 570 |
+
logger.warning(
|
| 571 |
+
"DeepSeek startup auth state: status=%s key=%s",
|
| 572 |
+
auth_state["status"],
|
| 573 |
+
auth_state["key_suffix"],
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
_warmup_inference_task = asyncio.create_task(_warmup_inference_client())
|
| 577 |
_warmup_vectorstore_task = asyncio.create_task(_warmup_vectorstore())
|
| 578 |
+
_warmup_deepseek_auth_task = asyncio.create_task(_warmup_deepseek_auth())
|
| 579 |
|
| 580 |
# FIX(502): Set a readiness flag so /health reports the true state without
|
| 581 |
# triggering heavy init on every health-check ping from HF Spaces' proxy.
|
|
|
|
| 2334 |
# to decide whether to route traffic; a slow response or crash here causes 502.
|
| 2335 |
@app.get("/health")
|
| 2336 |
async def health_check():
|
| 2337 |
+
# Cached DeepSeek auth state is read dynamically (never imported by name):
|
| 2338 |
+
# the startup probe rebinds the holder, and this handler must see the
|
| 2339 |
+
# current object. Missing module or holder degrades to "unchecked".
|
| 2340 |
+
try:
|
| 2341 |
+
import startup_validation as _startup_validation_module
|
| 2342 |
+
cached_auth = (
|
| 2343 |
+
getattr(_startup_validation_module, "cached_deepseek_auth_state", None) or {}
|
| 2344 |
+
)
|
| 2345 |
+
except Exception:
|
| 2346 |
+
cached_auth = {}
|
| 2347 |
return {
|
| 2348 |
"status": "ready" if _backend_ready else "starting",
|
| 2349 |
"space": "mathpulse-ai",
|
| 2350 |
"firebase": _firebase_ready,
|
| 2351 |
"chat_model": CHAT_MODEL,
|
| 2352 |
"risk_model": RISK_MODEL,
|
| 2353 |
+
"deepseek": {
|
| 2354 |
+
"status": cached_auth.get("status", "unchecked"),
|
| 2355 |
+
"key_suffix": cached_auth.get("key_suffix", "****"),
|
| 2356 |
+
"checked_at": cached_auth.get("checked_at"),
|
| 2357 |
+
},
|
| 2358 |
}
|
| 2359 |
|
| 2360 |
|
startup_validation.py
CHANGED
|
@@ -11,7 +11,12 @@ that's visible in HF Space logs.
|
|
| 11 |
import os
|
| 12 |
import sys
|
| 13 |
import logging
|
|
|
|
|
|
|
| 14 |
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
try:
|
| 17 |
from dotenv import load_dotenv
|
|
@@ -28,12 +33,104 @@ except Exception:
|
|
| 28 |
|
| 29 |
logger = logging.getLogger("mathpulse.startup")
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
class StartupError(Exception):
|
| 33 |
"""Critical error during startup validation."""
|
| 34 |
pass
|
| 35 |
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
def validate_imports() -> None:
|
| 38 |
"""Verify all critical imports work. Use absolute imports."""
|
| 39 |
logger.info("🔍 Validating Python imports...")
|
|
|
|
| 11 |
import os
|
| 12 |
import sys
|
| 13 |
import logging
|
| 14 |
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
|
| 15 |
+
from datetime import datetime, timezone
|
| 16 |
from pathlib import Path
|
| 17 |
+
from typing import Literal, Optional, TypedDict
|
| 18 |
+
|
| 19 |
+
from openai import APIConnectionError, APIError, APITimeoutError
|
| 20 |
|
| 21 |
try:
|
| 22 |
from dotenv import load_dotenv
|
|
|
|
| 33 |
|
| 34 |
logger = logging.getLogger("mathpulse.startup")
|
| 35 |
|
| 36 |
+
AuthProbeStatus = Literal["ok", "auth_failed", "connection_failed", "unchecked"]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class DeepSeekAuthState(TypedDict):
|
| 40 |
+
status: AuthProbeStatus
|
| 41 |
+
key_suffix: str
|
| 42 |
+
checked_at: Optional[str]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
cached_deepseek_auth_state: DeepSeekAuthState = {
|
| 46 |
+
"status": "unchecked",
|
| 47 |
+
"key_suffix": "****",
|
| 48 |
+
"checked_at": None,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
|
| 52 |
class StartupError(Exception):
|
| 53 |
"""Critical error during startup validation."""
|
| 54 |
pass
|
| 55 |
|
| 56 |
|
| 57 |
+
def compute_key_fingerprint(api_key: Optional[str] = None) -> str:
|
| 58 |
+
configured_key = api_key if api_key is not None else os.getenv("DEEPSEEK_API_KEY", "")
|
| 59 |
+
return f"****{configured_key[-4:]}" if configured_key else "****"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _record_deepseek_auth_state(status: AuthProbeStatus, key_suffix: str) -> DeepSeekAuthState:
|
| 63 |
+
global cached_deepseek_auth_state
|
| 64 |
+
cached_deepseek_auth_state = {
|
| 65 |
+
"status": status,
|
| 66 |
+
"key_suffix": key_suffix,
|
| 67 |
+
"checked_at": datetime.now(timezone.utc).isoformat(),
|
| 68 |
+
}
|
| 69 |
+
return cached_deepseek_auth_state
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def validate_deepseek_auth(timeout_s: float = 5.0) -> DeepSeekAuthState:
|
| 73 |
+
api_key = os.getenv("DEEPSEEK_API_KEY", "")
|
| 74 |
+
key_suffix = compute_key_fingerprint(api_key)
|
| 75 |
+
if not api_key:
|
| 76 |
+
logger.warning("DeepSeek startup auth probe: status=unchecked key=%s", key_suffix)
|
| 77 |
+
return _record_deepseek_auth_state("unchecked", key_suffix)
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
from services import inference_client
|
| 81 |
+
|
| 82 |
+
executor = ThreadPoolExecutor(max_workers=1)
|
| 83 |
+
probe_future = executor.submit(
|
| 84 |
+
lambda: inference_client.get_deepseek_client().models.list()
|
| 85 |
+
)
|
| 86 |
+
try:
|
| 87 |
+
probe_future.result(timeout=max(timeout_s, 0.0))
|
| 88 |
+
finally:
|
| 89 |
+
executor.shutdown(wait=False, cancel_futures=True)
|
| 90 |
+
except APIError as exc:
|
| 91 |
+
if getattr(exc, "status_code", None) in (401, 403):
|
| 92 |
+
logger.warning(
|
| 93 |
+
"DeepSeek startup auth probe: status=auth_failed key=%s",
|
| 94 |
+
key_suffix,
|
| 95 |
+
)
|
| 96 |
+
return _record_deepseek_auth_state("auth_failed", key_suffix)
|
| 97 |
+
logger.warning(
|
| 98 |
+
"DeepSeek startup auth probe: status=connection_failed key=%s",
|
| 99 |
+
key_suffix,
|
| 100 |
+
)
|
| 101 |
+
return _record_deepseek_auth_state("connection_failed", key_suffix)
|
| 102 |
+
except (APIConnectionError, APITimeoutError, FutureTimeoutError, TimeoutError, OSError):
|
| 103 |
+
logger.warning(
|
| 104 |
+
"DeepSeek startup auth probe: status=connection_failed key=%s",
|
| 105 |
+
key_suffix,
|
| 106 |
+
)
|
| 107 |
+
return _record_deepseek_auth_state("connection_failed", key_suffix)
|
| 108 |
+
except ValueError:
|
| 109 |
+
logger.warning("DeepSeek startup auth probe: status=unchecked key=%s", key_suffix)
|
| 110 |
+
return _record_deepseek_auth_state("unchecked", key_suffix)
|
| 111 |
+
except Exception as exc:
|
| 112 |
+
if getattr(exc, "status_code", None) in (401, 403) or type(exc).__name__ == "InferenceAuthError":
|
| 113 |
+
logger.warning(
|
| 114 |
+
"DeepSeek startup auth probe: status=auth_failed key=%s",
|
| 115 |
+
key_suffix,
|
| 116 |
+
)
|
| 117 |
+
return _record_deepseek_auth_state("auth_failed", key_suffix)
|
| 118 |
+
if type(exc).__name__ == "InferenceConnectionError":
|
| 119 |
+
logger.warning(
|
| 120 |
+
"DeepSeek startup auth probe: status=connection_failed key=%s",
|
| 121 |
+
key_suffix,
|
| 122 |
+
)
|
| 123 |
+
return _record_deepseek_auth_state("connection_failed", key_suffix)
|
| 124 |
+
logger.warning(
|
| 125 |
+
"DeepSeek startup auth probe: status=connection_failed key=%s",
|
| 126 |
+
key_suffix,
|
| 127 |
+
)
|
| 128 |
+
return _record_deepseek_auth_state("connection_failed", key_suffix)
|
| 129 |
+
|
| 130 |
+
logger.info("DeepSeek startup auth probe: status=ok key=%s", key_suffix)
|
| 131 |
+
return _record_deepseek_auth_state("ok", key_suffix)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
def validate_imports() -> None:
|
| 135 |
"""Verify all critical imports work. Use absolute imports."""
|
| 136 |
logger.info("🔍 Validating Python imports...")
|
tests/test_startup_auth_visibility.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for DeepSeek startup authentication validation and health visibility.
|
| 2 |
+
|
| 3 |
+
These tests define the contract for startup credential validation and health visibility
|
| 4 |
+
before production implementation exists (RED state).
|
| 5 |
+
|
| 6 |
+
Contract under test:
|
| 7 |
+
(a) validate_deepseek_auth(timeout_s=5.0) when DeepSeek returns HTTP 401:
|
| 8 |
+
- returns a status representation with status == 'auth_failed'
|
| 9 |
+
- logs/captures key suffix in '****last4' format
|
| 10 |
+
- ensures the full fake key string never appears in logs or output
|
| 11 |
+
(b) GET /health when injected with cached auth state:
|
| 12 |
+
- returns additive dictionary under 'deepseek' key: {status, key_suffix, checked_at}
|
| 13 |
+
- contains only masked key suffix (e.g. '****1234')
|
| 14 |
+
- ensures full fake key string never appears in response body
|
| 15 |
+
(c) validate_deepseek_auth(timeout_s=5.0) when DeepSeek returns HTTP 200 / successful probe:
|
| 16 |
+
- returns status == 'ok'
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import logging
|
| 22 |
+
import os
|
| 23 |
+
from unittest.mock import MagicMock, patch
|
| 24 |
+
|
| 25 |
+
import httpx
|
| 26 |
+
import openai
|
| 27 |
+
import pytest
|
| 28 |
+
from fastapi.testclient import TestClient
|
| 29 |
+
|
| 30 |
+
# Ensure test env vars match existing test harness conventions
|
| 31 |
+
os.environ["DEEPSEEK_API_KEY"] = "mock-key-for-testing"
|
| 32 |
+
os.environ["INFERENCE_MAX_RETRIES"] = "1"
|
| 33 |
+
os.environ["INFERENCE_BACKGROUND_MAX_RETRIES"] = "1"
|
| 34 |
+
os.environ["INFERENCE_INTERACTIVE_MAX_RETRIES"] = "1"
|
| 35 |
+
os.environ["INFERENCE_BACKOFF_SEC"] = "0"
|
| 36 |
+
os.environ["INFERENCE_BACKGROUND_BACKOFF_SEC"] = "0"
|
| 37 |
+
|
| 38 |
+
import main as main_module
|
| 39 |
+
from main import app
|
| 40 |
+
|
| 41 |
+
# Test client configured following existing backend test patterns
|
| 42 |
+
client = TestClient(app, headers={"Authorization": "Bearer mock_token_teacher_T"})
|
| 43 |
+
|
| 44 |
+
FAKE_DEEPSEEK_KEY = "sk-fake-test-key-1234"
|
| 45 |
+
EXPECTED_MASKED_SUFFIX = "****1234"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@pytest.fixture(autouse=True)
|
| 49 |
+
def _setup_common_mocks():
|
| 50 |
+
"""Bypass Firebase token verification, time.sleep backoff, and curriculum retrieval."""
|
| 51 |
+
with patch.object(
|
| 52 |
+
main_module.firebase_auth,
|
| 53 |
+
"verify_id_token",
|
| 54 |
+
return_value={"uid": "teacher_T", "role": "teacher"},
|
| 55 |
+
), patch(
|
| 56 |
+
"routes.rag_routes.retrieve_lesson_pdf_context",
|
| 57 |
+
return_value=([], "mock_file"),
|
| 58 |
+
), patch(
|
| 59 |
+
"time.sleep",
|
| 60 |
+
return_value=None,
|
| 61 |
+
):
|
| 62 |
+
yield
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class TestStartupAuthVisibility:
|
| 66 |
+
"""Contract tests for DeepSeek auth-visibility at startup and /health."""
|
| 67 |
+
|
| 68 |
+
def test_mocked_401_startup_auth_check_yields_auth_failed_and_redacts_key(
|
| 69 |
+
self, caplog: pytest.LogCaptureFixture
|
| 70 |
+
):
|
| 71 |
+
"""(a) Mocked-401 startup auth check yields 'auth_failed' with masked suffix and no raw key."""
|
| 72 |
+
from startup_validation import (
|
| 73 |
+
compute_key_fingerprint,
|
| 74 |
+
validate_deepseek_auth,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
req = httpx.Request("POST", "https://api.deepseek.com")
|
| 78 |
+
api_error_401 = openai.APIError(
|
| 79 |
+
f"Authentication failed for key {EXPECTED_MASKED_SUFFIX}",
|
| 80 |
+
request=req,
|
| 81 |
+
body={"error": {"message": f"Invalid API key {EXPECTED_MASKED_SUFFIX}"}},
|
| 82 |
+
)
|
| 83 |
+
api_error_401.status_code = 401
|
| 84 |
+
|
| 85 |
+
mock_client = MagicMock()
|
| 86 |
+
mock_client.models.list.side_effect = api_error_401
|
| 87 |
+
mock_client.chat.completions.create.side_effect = api_error_401
|
| 88 |
+
|
| 89 |
+
fingerprint = compute_key_fingerprint(FAKE_DEEPSEEK_KEY)
|
| 90 |
+
assert EXPECTED_MASKED_SUFFIX in fingerprint
|
| 91 |
+
assert FAKE_DEEPSEEK_KEY not in fingerprint
|
| 92 |
+
|
| 93 |
+
caplog.clear()
|
| 94 |
+
with caplog.at_level(logging.DEBUG), patch.dict(
|
| 95 |
+
os.environ, {"DEEPSEEK_API_KEY": FAKE_DEEPSEEK_KEY}
|
| 96 |
+
), patch(
|
| 97 |
+
"services.inference_client.get_deepseek_client",
|
| 98 |
+
return_value=mock_client,
|
| 99 |
+
):
|
| 100 |
+
auth_status = validate_deepseek_auth(timeout_s=5.0)
|
| 101 |
+
|
| 102 |
+
status_value = (
|
| 103 |
+
auth_status.get("status")
|
| 104 |
+
if isinstance(auth_status, dict)
|
| 105 |
+
else getattr(auth_status, "status", None)
|
| 106 |
+
)
|
| 107 |
+
assert status_value == "auth_failed"
|
| 108 |
+
|
| 109 |
+
captured_logs = caplog.text
|
| 110 |
+
assert EXPECTED_MASKED_SUFFIX in captured_logs
|
| 111 |
+
assert FAKE_DEEPSEEK_KEY not in captured_logs
|
| 112 |
+
|
| 113 |
+
def test_health_endpoint_returns_additive_cached_deepseek_auth_state(self):
|
| 114 |
+
"""(b) GET /health returns additive deepseek: {status, key_suffix, checked_at} with masked key."""
|
| 115 |
+
import startup_validation
|
| 116 |
+
|
| 117 |
+
cached_state_holder = getattr(
|
| 118 |
+
startup_validation, "cached_deepseek_auth_state"
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
injected_state = {
|
| 122 |
+
"status": "auth_failed",
|
| 123 |
+
"key_suffix": EXPECTED_MASKED_SUFFIX,
|
| 124 |
+
"checked_at": "2026-09-22T00:00:00Z",
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
with patch.object(
|
| 128 |
+
startup_validation,
|
| 129 |
+
"cached_deepseek_auth_state",
|
| 130 |
+
injected_state,
|
| 131 |
+
), patch.dict(
|
| 132 |
+
os.environ, {"DEEPSEEK_API_KEY": FAKE_DEEPSEEK_KEY}
|
| 133 |
+
):
|
| 134 |
+
response = client.get("/health")
|
| 135 |
+
|
| 136 |
+
assert response.status_code == 200
|
| 137 |
+
response_body = response.json()
|
| 138 |
+
|
| 139 |
+
assert "deepseek" in response_body
|
| 140 |
+
deepseek_info = response_body["deepseek"]
|
| 141 |
+
assert deepseek_info.get("status") == "auth_failed"
|
| 142 |
+
assert deepseek_info.get("key_suffix") == EXPECTED_MASKED_SUFFIX
|
| 143 |
+
assert "checked_at" in deepseek_info
|
| 144 |
+
|
| 145 |
+
raw_response_text = response.text
|
| 146 |
+
assert EXPECTED_MASKED_SUFFIX in raw_response_text
|
| 147 |
+
assert FAKE_DEEPSEEK_KEY not in raw_response_text
|
| 148 |
+
|
| 149 |
+
def test_mocked_ok_startup_auth_check_yields_ok_status(self):
|
| 150 |
+
"""(c) Mocked-OK startup auth check yields status 'ok'."""
|
| 151 |
+
from startup_validation import validate_deepseek_auth
|
| 152 |
+
|
| 153 |
+
mock_client = MagicMock()
|
| 154 |
+
mock_client.models.list.return_value = MagicMock()
|
| 155 |
+
mock_client.chat.completions.create.return_value = MagicMock()
|
| 156 |
+
|
| 157 |
+
with patch.dict(
|
| 158 |
+
os.environ, {"DEEPSEEK_API_KEY": FAKE_DEEPSEEK_KEY}
|
| 159 |
+
), patch(
|
| 160 |
+
"services.inference_client.get_deepseek_client",
|
| 161 |
+
return_value=mock_client,
|
| 162 |
+
):
|
| 163 |
+
auth_status = validate_deepseek_auth(timeout_s=5.0)
|
| 164 |
+
|
| 165 |
+
status_value = (
|
| 166 |
+
auth_status.get("status")
|
| 167 |
+
if isinstance(auth_status, dict)
|
| 168 |
+
else getattr(auth_status, "status", None)
|
| 169 |
+
)
|
| 170 |
+
assert status_value == "ok"
|