From 20635bff061db6e97c6cccb6c445042264a0e697 Mon Sep 17 00:00:00 2001 From: JiXu Date: Sat, 6 Jun 2026 16:14:13 +0800 Subject: [PATCH] =?UTF-8?q?test(api):=20=E8=A1=A5=E5=8F=AC=E5=9B=9E=20SSE?= =?UTF-8?q?=20=E9=94=99=E8=AF=AF=E7=A0=81=E4=B8=8E=20/llm=20=E7=BC=BA?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=20404=20=E8=B7=AF=E7=94=B1=E5=87=BA=E5=8F=A3?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 关闭 feature-completion-audit 对 PR #136 发现的两处 Required 测试缺口(仅加测试,不动实现): - 召回路由 RecallFatalError → SSE RECALL_EMBEDDING_CONFIG_MISSING(方案明列的路由错误码映射) - /llm _resolve_provider 缺配置(含系统兜底未命中)→ HTTP 404 映射 PR #136 经 squash 合并 dev 时仅含 feature 本体,这两个出口测试补在此 PR。 --- tests/unit/api/test_llm_route_coercion.py | 24 ++++++++++++++++++++--- tests/unit/api/test_recall_route.py | 20 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) 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)