diff --git a/tests/unit/api/test_llm_route_coercion.py b/tests/unit/api/test_llm_route_coercion.py index ccf0d2e..1c945fd 100644 --- a/tests/unit/api/test_llm_route_coercion.py +++ b/tests/unit/api/test_llm_route_coercion.py @@ -1,14 +1,18 @@ # -*- coding: utf-8 -*- -"""/llm 路由边界 ID 归一:user_id / config_id 字符串 → int,非法值 → 422。 +"""/llm 路由边界行为:user_id / config_id 归一(M2/M3)+ 缺配置 → 404。 -锁定 M2/M3 修复:弱类型 ID 不再下沉到 SQL 靠驱动隐式转换,路由层显式校验。 +锁定 M2/M3 修复:弱类型 ID 不再下沉到 SQL 靠驱动隐式转换,路由层显式校验; +并验证统一解析未命中(含系统兜底)时翻成 HTTP 404,保持原有对外契约。 """ from __future__ import annotations +from unittest.mock import AsyncMock + import pytest from fastapi import HTTPException -from src.api.routes.llm import _coerce_int +from src.api.routes.llm import _coerce_int, _resolve_provider +from src.core.llm.exceptions import UserModelConfigMissingError def test_coerce_int_valid(): @@ -27,3 +31,17 @@ def test_coerce_int_rejects_empty(): _coerce_int("", "config_id") assert exc.value.status_code == 422 assert "config_id" in exc.value.detail + + +@pytest.mark.asyncio +async def test_resolve_provider_missing_config_maps_to_404(monkeypatch): + """统一解析未命中(含系统兜底)抛 UserModelConfigMissingError → HTTP 404, + 保持 /llm 端点原有对外行为。""" + async def _raise(**kwargs): + raise UserModelConfigMissingError("EMBEDDING", 123) + + monkeypatch.setattr("src.api.routes.llm.aresolve_user_model", _raise) + with pytest.raises(HTTPException) as exc: + await _resolve_provider(db=AsyncMock(), user_id="123", capability="EMBEDDING") + assert exc.value.status_code == 404 + assert "EMBEDDING" in exc.value.detail diff --git a/tests/unit/api/test_recall_route.py b/tests/unit/api/test_recall_route.py index 86555cb..ec9ca14 100644 --- a/tests/unit/api/test_recall_route.py +++ b/tests/unit/api/test_recall_route.py @@ -17,6 +17,7 @@ from src.config import settings from src.core.pipeline.recall import ( RecallError, + RecallFatalError, RecallHit, RecallResponse, ) @@ -295,6 +296,25 @@ def test_all_sources_failed_emits_sse_error(client): app.dependency_overrides.pop(get_recall_pipeline, None) +def test_embedding_config_missing_emits_sse_error(client): + """发起用户无默认 EMBEDDING 配置 → pipeline 抛 RecallFatalError → + SSE error 事件返回 RECALL_EMBEDDING_CONFIG_MISSING(硬失败,不降级)。""" + fake = FakePipeline(exc=RecallFatalError("user embedding config missing")) + app.dependency_overrides[get_recall_pipeline] = lambda: fake + try: + resp = _post(TestClient(app), make_token(), {"query": "q", "user_id": 123, "dataset_ids": [1]}) + assert resp.status_code == 200 + assert resp.headers["content-type"].startswith("text/event-stream") + import json + name, data = _parse_sse(resp.text)[0] + assert name == "error" + payload = json.loads(data) + assert payload["code"] == "RECALL_EMBEDDING_CONFIG_MISSING" + assert "Traceback" not in payload["message"] + finally: + app.dependency_overrides.pop(get_recall_pipeline, None) + + def test_timeout_emits_sse_error(client, monkeypatch): monkeypatch.setattr(settings, "RECALL_STREAM_TIMEOUT_MS", 10) fake = FakePipeline(response=_ok_response(), delay=0.5)