From e349f2ba9bea961d1310dcacc9eb788a406ce08a Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Wed, 9 Sep 2026 07:12:54 +0900 Subject: [PATCH] =?UTF-8?q?fix(settings):=20=E7=AB=AF=E7=82=B9=E6=B5=8B?= =?UTF-8?q?=E9=80=9F=E5=9C=A8=E6=B8=85=E5=8D=95=E7=BC=93=E5=AD=98=E6=9C=AA?= =?UTF-8?q?=E9=A2=84=E7=83=AD=E6=97=B6=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _endpoints_cache 初值是 {"ts": 0.0, "data": None}, "data" 键存在, 所以 dict.get("data", {}) 的默认值不生效, 返回的是 None。进程启动后到第一次 GET /api/settings/endpoints 之前, POST /api/settings/test_endpoint 不带 rounds 会在 None.get("testRounds", 5) 上抛 AttributeError → HTTP 500。 前端 endpoints 查询有 5 分钟 staleTime, 后端重启后用户再点测速正好落在 这个窗口: 前端不会重新拉清单, 后端缓存却已清空。 改为先 `or {}` 兜底再取 testRounds, 保持原有默认 5 轮语义。 --- backend/app/api/settings.py | 4 +- .../tests/test_endpoint_latency_cold_cache.py | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_endpoint_latency_cold_cache.py diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index 305b04d..299035c 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -1666,7 +1666,9 @@ async def test_endpoint(req: TestEndpointIn) -> dict: import statistics base = req.url.rstrip("/") - rounds = max(1, min(10, req.rounds or _endpoints_cache.get("data", {}).get("testRounds", 5))) + # 缓存初值的 "data" 是 None(键存在, get 的默认值不生效), 端点清单未预热时要兜底 + manifest = _endpoints_cache.get("data") or {} + rounds = max(1, min(10, req.rounds or manifest.get("testRounds", 5))) health_url = base + "/health" latencies: list[float] = [] diff --git a/backend/tests/test_endpoint_latency_cold_cache.py b/backend/tests/test_endpoint_latency_cold_cache.py new file mode 100644 index 0000000..ca1e531 --- /dev/null +++ b/backend/tests/test_endpoint_latency_cold_cache.py @@ -0,0 +1,48 @@ +"""端点测速接口: 端点清单缓存未预热时的默认轮数不能崩。 + +_endpoints_cache 的初值是 {"ts": 0.0, "data": None} —— 进程启动后直到第一次 +GET /api/settings/endpoints 之前, "data" 键存在但值为 None。前端 endpoints 查询 +有 5 分钟 staleTime, 后端(看门狗自愈/重启)重启后用户再点测速, 就落在这个窗口里。 +""" +from __future__ import annotations + +import pytest + +from app.api import settings as settings_api + + +@pytest.fixture(autouse=True) +def _cold_cache_and_fake_ping(monkeypatch): + """还原进程刚启动时的缓存初值, 并把网络探测换成固定延迟。""" + monkeypatch.setattr(settings_api, "_endpoints_cache", {"ts": 0.0, "data": None}) + + async def _ping(url: str, timeout: float = 10.0) -> float: + return 12.5 + + monkeypatch.setattr(settings_api, "_http_ping", _ping) + + +async def test_latency_test_falls_back_to_default_rounds_on_cold_cache(): + """缓存未预热且请求不带 rounds 时, 回退到默认 5 轮而不是 500。""" + result = await settings_api.test_endpoint( + settings_api.TestEndpointIn(url="https://api.example.com"), + ) + + assert result["ok"] is True + assert result["rounds"] == 5 + assert result["success"] == 5 + assert result["median_ms"] == 12.5 + + +async def test_latency_test_uses_manifest_rounds_when_cache_is_warm(monkeypatch): + """缓存已预热时仍取 endpoints.json 的 testRounds。""" + monkeypatch.setattr( + settings_api, "_endpoints_cache", {"ts": 0.0, "data": {"testRounds": 2}}, + ) + + result = await settings_api.test_endpoint( + settings_api.TestEndpointIn(url="https://api.example.com"), + ) + + assert result["rounds"] == 2 + assert result["success"] == 2