fix(modules): 默认服务配置改为尊重显式标记,不再按读取顺序取第一个 (#6431)

This commit is contained in:
Aqr-K
2026-08-24 08:03:28 +08:00
committed by GitHub
parent 4e44deb0fa
commit 4a5958c3c2
2 changed files with 128 additions and 2 deletions
+11 -2
View File
@@ -202,9 +202,18 @@ class ServiceBase(Generic[TService, TConf], metaclass=ABCMeta):
"""
获取默认服务配置的名称
:return: 默认第一个配置的名称
优先返回被显式标记为默认的配置。只有在没有任何配置带标记时,才退回到第一个配置——
配置的先后来自读取顺序,用户既看不见也控制不了,删掉一个配置会让「默认」静默改指
另一个,因此它只能作为无标记时的兜底,不能盖过用户的显式选择。
:return: 默认配置的名称,无配置时为 None
"""
# 默认使用第一个配置的名称
marked = next(
(conf for conf in self._configs.values() if getattr(conf, "default", False)),
None,
)
if marked:
return marked.name
first_conf = next(iter(self._configs.values()), None)
return first_conf.name if first_conf else None
+117
View File
@@ -0,0 +1,117 @@
"""默认服务配置的选取必须尊重用户的显式标记。
配置的先后来自读取顺序,用户既看不见也控制不了。按顺序取第一个意味着:删掉一个无关配置,
「默认下载器」就会静默改指另一个,而下载任务的归属记录已经按旧的默认写进了历史。
"""
from typing import Dict, Optional
import pytest
from app.modules import ServiceBase
from app.schemas.system import DownloaderConf
class _StubService(ServiceBase[object, DownloaderConf]):
"""只提供配置字典的服务基类桩,不连接任何真实后端。"""
def __init__(self, configs: Dict[str, DownloaderConf]) -> None:
"""记录待测配置,跳过真实服务实例化。
:param configs: 名称到配置的映射,顺序即读取顺序
"""
self._configs = configs
self._instances = {}
self._service_name = "qbittorrent"
def get_configs(self) -> Dict[str, DownloaderConf]:
"""返回构造时传入的配置字典。
:return: 名称到配置的映射
"""
return self._configs
def _conf(name: str, *, default: bool = False) -> DownloaderConf:
"""构造一份已启用的下载器配置。
:param name: 配置名称
:param default: 是否标记为默认
:return: 下载器配置
"""
return DownloaderConf(
name=name,
type="qbittorrent",
enabled=True,
default=default,
config={},
)
def test_no_config_has_no_default() -> None:
"""没有任何配置时不应凭空给出默认名称。"""
service = _StubService({})
assert service.get_default_config_name() is None
def test_single_config_is_the_default() -> None:
"""只有一份配置时它就是默认,无需用户额外标记。"""
service = _StubService({"only": _conf("only")})
assert service.get_default_config_name() == "only"
def test_marked_config_wins_over_order() -> None:
"""标记为默认的配置优先,即使它不排在最前。"""
service = _StubService(
{
"first": _conf("first"),
"second": _conf("second", default=True),
}
)
assert service.get_default_config_name() == "second"
def test_marked_config_survives_reordering() -> None:
"""读取顺序变化不得改变已标记的默认配置。
这是本修复要守住的性质:顺序是实现细节,标记才是用户的意思。
"""
marked = _conf("marked", default=True)
plain = _conf("plain")
forward = _StubService({"marked": marked, "plain": plain})
backward = _StubService({"plain": plain, "marked": marked})
assert forward.get_default_config_name() == "marked"
assert backward.get_default_config_name() == "marked"
def test_unmarked_configs_fall_back_to_first() -> None:
"""一个标记都没有时保持原有行为,不改变既有部署的默认目标。"""
service = _StubService(
{
"alpha": _conf("alpha"),
"beta": _conf("beta"),
}
)
assert service.get_default_config_name() == "alpha"
@pytest.mark.parametrize("name", ["alpha", "beta"])
def test_get_config_without_name_follows_the_default(name: str) -> None:
"""不指定名称时取到的必须是默认配置本身。"""
service = _StubService(
{
"alpha": _conf("alpha", default=name == "alpha"),
"beta": _conf("beta", default=name == "beta"),
}
)
resolved: Optional[DownloaderConf] = service.get_config()
assert resolved is not None
assert resolved.name == name